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.68.1
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.ClientStreamTracer;
import io.grpc.ClientTransportFilter;
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 io.grpc.SynchronizationContext.ScheduledHandle;
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.TimeUnit;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;

/**
 * Transports for a single {@link SocketAddress}.
 */
@ThreadSafe
final class InternalSubchannel implements InternalInstrumented, TransportProvider {

  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;

  private final List transportFilters;

  /**
   * All field must be mutated in the syncContext.
   */
  private final SynchronizationContext syncContext;

  /**
   * The index of the address corresponding to pendingTransport/activeTransport, or at beginning if
   * both are null.
   *
   * 

Note: any {@link Index#updateAddresses(List)} should also update {@link #addressGroups}. */ private final Index addressIndex; /** * A volatile accessor to {@link Index#getAddressGroups()}. There are few methods ({@link * #getAddressGroups()} and {@link #toString()} access this value where they supposed to access * in the {@link #syncContext}. Ideally {@link Index#getAddressGroups()} can be volatile, so we * don't need to maintain this volatile accessor. Although, having this accessor can reduce * unnecessary volatile reads while it delivers clearer intention of why . */ private volatile List addressGroups; /** * The policy to control back off between reconnects. Non-{@code null} when a reconnect task is * scheduled. */ private BackoffPolicy reconnectPolicy; /** * Timer monitoring duration since entering CONNECTING state. */ private final Stopwatch connectingTimer; @Nullable private ScheduledHandle reconnectTask; @Nullable private ScheduledHandle shutdownDueToUpdateTask; @Nullable private ManagedClientTransport shutdownDueToUpdateTransport; /** * 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. */ 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. */ @Nullable private ConnectionClientTransport pendingTransport; /** * The transport for new outgoing requests. Non-null only in READY state. */ @Nullable private volatile ManagedClientTransport activeTransport; private volatile ConnectivityStateInfo state = ConnectivityStateInfo.forNonError(IDLE); 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, ChannelLogger channelLogger, List transportFilters) { Preconditions.checkNotNull(addressGroups, "addressGroups"); Preconditions.checkArgument(!addressGroups.isEmpty(), "addressGroups is empty"); checkListHasNoNulls(addressGroups, "addressGroups contains null entry"); List unmodifiableAddressGroups = Collections.unmodifiableList(new ArrayList<>(addressGroups)); this.addressGroups = unmodifiableAddressGroups; this.addressIndex = new Index(unmodifiableAddressGroups); 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 = Preconditions.checkNotNull(logId, "logId"); this.channelLogger = Preconditions.checkNotNull(channelLogger, "channelLogger"); this.transportFilters = transportFilters; } ChannelLogger getChannelLogger() { return channelLogger; } @Override public ClientTransport obtainActiveTransport() { ClientTransport savedTransport = activeTransport; if (savedTransport != null) { return savedTransport; } syncContext.execute(new Runnable() { @Override public void run() { if (state.getState() == IDLE) { channelLogger.log(ChannelLogLevel.INFO, "CONNECTING as requested"); gotoNonErrorState(CONNECTING); startNewTransport(); } } }); 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; } private void startNewTransport() { syncContext.throwIfNotInThisSynchronizationContext(); 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(); } Attributes currentEagAttributes = addressIndex.getCurrentEagAttributes(); String eagChannelAuthority = currentEagAttributes .get(EquivalentAddressGroup.ATTR_AUTHORITY_OVERRIDE); ClientTransportFactory.ClientTransportOptions options = new ClientTransportFactory.ClientTransportOptions() .setAuthority(eagChannelAuthority != null ? eagChannelAuthority : authority) .setEagAttributes(currentEagAttributes) .setUserAgent(userAgent) .setHttpConnectProxiedSocketAddress(proxiedAddr); TransportLogger transportLogger = new TransportLogger(); // In case the transport logs in the constructor, use the subchannel logId transportLogger.logId = getLogId(); ConnectionClientTransport transport = new CallTracingTransport( transportFactory .newClientTransport(address, options, transportLogger), callsTracer); transportLogger.logId = transport.getLogId(); channelz.addClientSocket(transport); pendingTransport = transport; transports.add(transport); Runnable runnable = transport.start(new TransportListener(transport)); if (runnable != null) { syncContext.executeLater(runnable); } channelLogger.log(ChannelLogLevel.INFO, "Started transport {0}", transportLogger.logId); } /** * Only called after all addresses attempted and failed (TRANSIENT_FAILURE). * @param status the causal status when the channel begins transition to * TRANSIENT_FAILURE. */ private void scheduleBackoff(final Status status) { syncContext.throwIfNotInThisSynchronizationContext(); class EndOfCurrentBackoff implements Runnable { @Override public void run() { reconnectTask = null; channelLogger.log(ChannelLogLevel.INFO, "CONNECTING after backoff"); gotoNonErrorState(CONNECTING); startNewTransport(); } } 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"); reconnectTask = syncContext.schedule( new EndOfCurrentBackoff(), delayNanos, TimeUnit.NANOSECONDS, scheduledExecutor); } /** * Immediately attempt to reconnect if the current state is TRANSIENT_FAILURE. Otherwise this * method has no effect. */ void resetConnectBackoff() { syncContext.execute(new Runnable() { @Override public void run() { if (state.getState() != TRANSIENT_FAILURE) { return; } cancelReconnectTask(); channelLogger.log(ChannelLogLevel.INFO, "CONNECTING; backoff interrupted"); gotoNonErrorState(CONNECTING); startNewTransport(); } }); } private void gotoNonErrorState(final ConnectivityState newState) { syncContext.throwIfNotInThisSynchronizationContext(); gotoState(ConnectivityStateInfo.forNonError(newState)); } private void gotoState(final ConnectivityStateInfo newState) { syncContext.throwIfNotInThisSynchronizationContext(); if (state.getState() != newState.getState()) { Preconditions.checkState(state.getState() != SHUTDOWN, "Cannot transition out of SHUTDOWN to " + newState); state = newState; callback.onStateChange(InternalSubchannel.this, newState); } } /** Replaces the existing addresses, avoiding unnecessary reconnects. */ public void updateAddresses(final List newAddressGroups) { Preconditions.checkNotNull(newAddressGroups, "newAddressGroups"); checkListHasNoNulls(newAddressGroups, "newAddressGroups contains null entry"); Preconditions.checkArgument(!newAddressGroups.isEmpty(), "newAddressGroups is empty"); final List newImmutableAddressGroups = Collections.unmodifiableList(new ArrayList<>(newAddressGroups)); syncContext.execute(new Runnable() { @Override public void run() { ManagedClientTransport savedTransport = null; SocketAddress previousAddress = addressIndex.getCurrentAddress(); addressIndex.updateGroups(newImmutableAddressGroups); addressGroups = newImmutableAddressGroups; 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 { pendingTransport.shutdown( Status.UNAVAILABLE.withDescription( "InternalSubchannel closed pending transport due to address change")); pendingTransport = null; addressIndex.reset(); startNewTransport(); } } } if (savedTransport != null) { if (shutdownDueToUpdateTask != null) { // Keeping track of multiple shutdown tasks adds complexity, and shouldn't generally be // necessary. This transport has probably already had plenty of time. shutdownDueToUpdateTransport.shutdown( Status.UNAVAILABLE.withDescription( "InternalSubchannel closed transport early due to address change")); shutdownDueToUpdateTask.cancel(); shutdownDueToUpdateTask = null; shutdownDueToUpdateTransport = null; } // Avoid needless RPC failures by delaying the shutdown. See // https://github.com/grpc/grpc-java/issues/2562 shutdownDueToUpdateTransport = savedTransport; shutdownDueToUpdateTask = syncContext.schedule( new Runnable() { @Override public void run() { ManagedClientTransport transport = shutdownDueToUpdateTransport; shutdownDueToUpdateTask = null; shutdownDueToUpdateTransport = null; transport.shutdown( Status.UNAVAILABLE.withDescription( "InternalSubchannel closed transport due to address change")); } }, ManagedChannelImpl.SUBCHANNEL_SHUTDOWN_DELAY_SECONDS, TimeUnit.SECONDS, scheduledExecutor); } } }); } public void shutdown(final Status reason) { syncContext.execute(new Runnable() { @Override public void run() { ManagedClientTransport savedActiveTransport; ConnectionClientTransport savedPendingTransport; if (state.getState() == SHUTDOWN) { return; } shutdownReason = reason; savedActiveTransport = activeTransport; savedPendingTransport = pendingTransport; activeTransport = null; pendingTransport = null; gotoNonErrorState(SHUTDOWN); addressIndex.reset(); if (transports.isEmpty()) { handleTermination(); } // else: the callback will be run once all transports have been terminated cancelReconnectTask(); if (shutdownDueToUpdateTask != null) { shutdownDueToUpdateTask.cancel(); shutdownDueToUpdateTransport.shutdown(reason); shutdownDueToUpdateTask = null; shutdownDueToUpdateTransport = null; } 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. return MoreObjects.toStringHelper(this) .add("logId", logId.getId()) .add("addressGroups", addressGroups) .toString(); } private void handleTermination() { syncContext.execute(new Runnable() { @Override public void run() { channelLogger.log(ChannelLogLevel.INFO, "Terminated"); 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(final Status reason) { shutdown(reason); syncContext.execute(new Runnable() { @Override public void run() { Collection transportsCopy = new ArrayList(transports); for (ManagedClientTransport transport : transportsCopy) { transport.shutdownNow(reason); } } }); } List getAddressGroups() { return addressGroups; } private void cancelReconnectTask() { syncContext.throwIfNotInThisSynchronizationContext(); if (reconnectTask != null) { reconnectTask.cancel(); reconnectTask = null; reconnectPolicy = null; } } @Override public InternalLogId getLogId() { return logId; } @Override public ListenableFuture getStats() { final SettableFuture channelStatsFuture = SettableFuture.create(); syncContext.execute(new Runnable() { @Override public void run() { ChannelStats.Builder builder = new ChannelStats.Builder(); List addressGroupsSnapshot = addressIndex.getGroups(); List transportsSnapshot = new ArrayList(transports); builder.setTarget(addressGroupsSnapshot.toString()).setState(getState()); builder.setSockets(transportsSnapshot); callsTracer.updateBuilder(builder); channelTracer.updateBuilder(builder); channelStatsFuture.set(builder.build()); } }); return channelStatsFuture; } ConnectivityState getState() { return state.getState(); } 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; boolean shutdownInitiated = false; TransportListener(ConnectionClientTransport transport) { this.transport = transport; } @Override public Attributes filterTransport(Attributes attributes) { for (ClientTransportFilter filter : transportFilters) { attributes = Preconditions.checkNotNull(filter.transportReady(attributes), "Filter %s returned null", filter); } return attributes; } @Override public void transportReady() { channelLogger.log(ChannelLogLevel.INFO, "READY"); syncContext.execute(new Runnable() { @Override public void run() { reconnectPolicy = null; if (shutdownReason != null) { // activeTransport should have already been set to null by shutdown(). We keep it null. Preconditions.checkState(activeTransport == null, "Unexpected non-null activeTransport"); transport.shutdown(shutdownReason); } else if (pendingTransport == transport) { activeTransport = transport; pendingTransport = null; gotoNonErrorState(READY); } } }); } @Override public void transportInUse(boolean inUse) { handleTransportInUseState(transport, inUse); } @Override public void transportShutdown(final Status s) { channelLogger.log( ChannelLogLevel.INFO, "{0} SHUTDOWN with {1}", transport.getLogId(), printShortStatus(s)); shutdownInitiated = true; syncContext.execute(new Runnable() { @Override public void run() { if (state.getState() == SHUTDOWN) { return; } if (activeTransport == transport) { activeTransport = null; addressIndex.reset(); gotoNonErrorState(IDLE); } 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(); } } } }); } @Override public void transportTerminated() { Preconditions.checkState( shutdownInitiated, "transportShutdown() must be called before transportTerminated()."); channelLogger.log(ChannelLogLevel.INFO, "{0} Terminated", transport.getLogId()); channelz.removeClientSocket(transport); handleTransportInUseState(transport, false); for (ClientTransportFilter filter : transportFilters) { filter.transportTerminated(transport.getAttributes()); } syncContext.execute(new Runnable() { @Override public void run() { transports.remove(transport); if (state.getState() == SHUTDOWN && transports.isEmpty()) { handleTermination(); } } }); } } // 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, ClientStreamTracer[] tracers) { final ClientStream streamDelegate = super.newStream(method, headers, callOptions, tracers); 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, 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(")"); } if (status.getCause() != null) { buffer.append("[").append(status.getCause()).append("]"); } return buffer.toString(); } @VisibleForTesting static final class TransportLogger extends ChannelLogger { // Changed just after construction to break a cyclic dependency. InternalLogId logId; @Override public void log(ChannelLogLevel level, String message) { ChannelLoggerImpl.logOnly(logId, level, message); } @Override public void log(ChannelLogLevel level, String messageFormat, Object... args) { ChannelLoggerImpl.logOnly(logId, level, messageFormat, args); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy