mirror of https://github.com/grpc/grpc-java.git
xds: implement lb policy backend metric api (#5639)
* implemented utility methods to create ClientStreamTracer.Factory with OrcaReportListener installed for retrieving per-request ORCA data * added unit tests * use delegatesTo instead of spy * implemented OrcaReportingHelper delegating to some original Helper for load balancing policies accessing OOB metric reports * added unit tests for out-of-band ORCA metric accessing API in a separate test class * rebase to master, resolve the breaking change of StreamInfo class being final with builder * trashed hashCode/equal for OrcaReportingConfig * changed log level and channel trace event level to ERROR as required by design doc * added OrcaReportingHelperWrapper layer to allow updating report interval at any time * reverse the naming of parent/child helper, child helper is the outer-most helper in the wrapping structure * changed orca listener interface to use separate listener interfaces for per-request and out-of-band cases * added more comprehensive unit tests * added test case for per-request reporting that parent creates its own stream tracer * fixed bug of directly assign reporting config, which would cause it be mutated later * separate test cases for updating reporting config at different time * fixed lint style error * polish comments * minor polish in unit tests * refactor OrcaUtil class into OrcaOobUtil and OrcaPerRequestUtil and get rid of static methods for easier user testing * hide BackoffPolicyProvider and Stopwatch supplier in OrcaOobUtil's public API * add javadoc for getInstance() methods * ensure the same Subchannel instance created by the helper that has corresponding OrcaOobReportListener registered are passed to the listener callback * removed costNames foe OrcaReportingConfig * removed redundant checks * reformated the OrcaOobUtilTest class to put helper methods in the bottom * fixed impl with changes made on Subchannel (SubchannelStateListener now ties with Subchannel) * fixed comments * added usage examples in javadoc for OrcaUtils * add method comments for OrcaUtil's listener API threading * make fields in OrcaReportingConfig final * fixed OrcaOobUtilTest for calling setOrcaReportingConfig inside syncContext * added ExperimentalApi annotation for Orca utils
This commit is contained in:
parent
46f34a513f
commit
7fd5f261b4
|
|
@ -0,0 +1,629 @@
|
|||
/*
|
||||
* Copyright 2019 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.xds;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static io.grpc.ConnectivityState.IDLE;
|
||||
import static io.grpc.ConnectivityState.READY;
|
||||
import static io.grpc.ConnectivityState.SHUTDOWN;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.base.Stopwatch;
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.protobuf.util.Durations;
|
||||
import io.envoyproxy.udpa.data.orca.v1.OrcaLoadReport;
|
||||
import io.envoyproxy.udpa.service.orca.v1.OpenRcaServiceGrpc;
|
||||
import io.envoyproxy.udpa.service.orca.v1.OrcaLoadReportRequest;
|
||||
import io.grpc.CallOptions;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.ChannelLogger;
|
||||
import io.grpc.ChannelLogger.ChannelLogLevel;
|
||||
import io.grpc.ClientCall;
|
||||
import io.grpc.ConnectivityStateInfo;
|
||||
import io.grpc.ExperimentalApi;
|
||||
import io.grpc.LoadBalancer;
|
||||
import io.grpc.LoadBalancer.CreateSubchannelArgs;
|
||||
import io.grpc.LoadBalancer.Helper;
|
||||
import io.grpc.LoadBalancer.Subchannel;
|
||||
import io.grpc.LoadBalancer.SubchannelStateListener;
|
||||
import io.grpc.Metadata;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.Status.Code;
|
||||
import io.grpc.SynchronizationContext;
|
||||
import io.grpc.SynchronizationContext.ScheduledHandle;
|
||||
import io.grpc.internal.BackoffPolicy;
|
||||
import io.grpc.internal.ExponentialBackoffPolicy;
|
||||
import io.grpc.internal.GrpcUtil;
|
||||
import io.grpc.util.ForwardingLoadBalancerHelper;
|
||||
import io.grpc.util.ForwardingSubchannel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Utility class that provides method for {@link LoadBalancer} to install listeners to receive
|
||||
* out-of-band backend cost metrics in the format of Open Request Cost Aggregation (ORCA).
|
||||
*/
|
||||
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/5790")
|
||||
public abstract class OrcaOobUtil {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(OrcaPerRequestUtil.class.getName());
|
||||
private static final OrcaOobUtil DEFAULT_INSTANCE =
|
||||
new OrcaOobUtil() {
|
||||
|
||||
@Override
|
||||
public OrcaReportingHelperWrapper newOrcaReportingHelperWrapper(
|
||||
LoadBalancer.Helper delegate,
|
||||
OrcaOobReportListener listener) {
|
||||
return newOrcaReportingHelperWrapper(
|
||||
delegate,
|
||||
listener,
|
||||
new ExponentialBackoffPolicy.Provider(),
|
||||
GrpcUtil.STOPWATCH_SUPPLIER);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets an {@code OrcaOobUtil} instance that provides actual implementation of
|
||||
* {@link #newOrcaReportingHelperWrapper}.
|
||||
*/
|
||||
public static OrcaOobUtil getInstance() {
|
||||
return DEFAULT_INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link LoadBalancer.Helper} with provided {@link OrcaOobReportListener} installed
|
||||
* to receive callback when an out-of-band ORCA report is received.
|
||||
*
|
||||
* <p>Example usages:
|
||||
*
|
||||
* <ul>
|
||||
* <li> Leaf policy (e.g., WRR policy)
|
||||
* <pre>
|
||||
* {@code
|
||||
* class WrrLoadbalancer extends LoadBalancer {
|
||||
* private final Helper originHelper; // the original Helper
|
||||
*
|
||||
* public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) {
|
||||
* // listener implements the logic for WRR's usage of backend metrics.
|
||||
* OrcaReportingHelperWrapper orcaWrapper =
|
||||
* OrcaOobUtil.getInstance().newOrcaReportingHelperWrapper(originHelper, listener);
|
||||
* orcaWrapper.setReportingConfig(
|
||||
* OrcaRerportingConfig.newBuilder().setReportInterval(30, SECOND).build());
|
||||
* Subchannel subchannel =
|
||||
* orcaWrapper.asHelper().createSubchannel(CreateSubchannelArgs.newBuilder()...);
|
||||
* ...
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* </li>
|
||||
* <li> Delegating policy doing per-child-policy aggregation
|
||||
* <pre>
|
||||
* {@code
|
||||
* class XdsLoadBalancer extends LoadBalancer {
|
||||
* private final Helper originHelper; // the original Helper
|
||||
*
|
||||
* private void createChildPolicy(
|
||||
* Locality locality, LoadBalancerProvider childPolicyProvider) {
|
||||
* // Each Locality has a child policy, and the parent does per-locality aggregation by
|
||||
* // summing everything up.
|
||||
*
|
||||
* // Create an OrcaReportingHelperWrapper for each Locality.
|
||||
* // listener implements the logic for locality-level backend metric aggregation.
|
||||
* OrcaReportingHelperWrapper orcaWrapper =
|
||||
* OrcaOobUtil.getInstance().newOrcaReportingHelperWrapper(originHelper, listener);
|
||||
* orcaWrapper.setReportingConfig(
|
||||
* OrcaRerportingConfig.newBuilder().setReportInterval(30, SECOND).build());
|
||||
* LoadBalancer childLb = childPolicyProvider.newLoadBalancer(orcaWrapper.asHelper());
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
* @param delegate the delegate helper that provides essentials for establishing subchannels to
|
||||
* backends.
|
||||
* @param listener contains the callback to be invoked when an out-of-band ORCA report is
|
||||
* received.
|
||||
*/
|
||||
public abstract OrcaReportingHelperWrapper newOrcaReportingHelperWrapper(
|
||||
LoadBalancer.Helper delegate,
|
||||
OrcaOobReportListener listener);
|
||||
|
||||
@VisibleForTesting
|
||||
static OrcaReportingHelperWrapper newOrcaReportingHelperWrapper(
|
||||
LoadBalancer.Helper delegate,
|
||||
OrcaOobReportListener listener,
|
||||
BackoffPolicy.Provider backoffPolicyProvider,
|
||||
Supplier<Stopwatch> stopwatchSupplier) {
|
||||
final OrcaReportingHelper orcaHelper =
|
||||
new OrcaReportingHelper(delegate, listener, backoffPolicyProvider, stopwatchSupplier);
|
||||
|
||||
return new OrcaReportingHelperWrapper() {
|
||||
@Override
|
||||
public void setReportingConfig(OrcaReportingConfig config) {
|
||||
orcaHelper.setReportingConfig(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Helper asHelper() {
|
||||
return orcaHelper;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The listener interface for receiving out-of-band ORCA reports from backends. The class that is
|
||||
* interested in processing backend cost metrics implements this interface, and the object created
|
||||
* with that class is registered with a component, using methods in {@link OrcaPerRequestUtil}.
|
||||
* When an ORCA report is received, that object's {@code onLoadReport} method is invoked.
|
||||
*/
|
||||
public interface OrcaOobReportListener {
|
||||
|
||||
/**
|
||||
* Invoked when an out-of-band ORCA report is received.
|
||||
*
|
||||
* <p>Note this callback will be invoked from the {@link SynchronizationContext} of the
|
||||
* delegated helper, implementations should not block.
|
||||
*
|
||||
* @param report load report in the format of ORCA protocol.
|
||||
*/
|
||||
void onLoadReport(OrcaLoadReport report);
|
||||
}
|
||||
|
||||
/**
|
||||
* Blueprint for the wrapper that wraps a {@link LoadBalancer.Helper} with the capability of
|
||||
* allowing {@link LoadBalancer}s interested in receiving out-of-band ORCA reports to update the
|
||||
* reporting configuration such as reporting interval.
|
||||
*/
|
||||
public abstract static class OrcaReportingHelperWrapper {
|
||||
|
||||
/**
|
||||
* Sets the configuration of receiving ORCA reports, such as the interval of receiving reports.
|
||||
*
|
||||
* <p>This method needs to be called from the SynchronizationContext returned by the wrapped
|
||||
* helper's {@link Helper#getSynchronizationContext()}.
|
||||
*
|
||||
* <p>Each load balancing policy must call this method to configure the backend load reporting.
|
||||
* Otherwise, it will not receive ORCA reports.
|
||||
*
|
||||
* <p>If multiple load balancing policies configure reporting with different intervals, reports
|
||||
* come with the minimum of those intervals.
|
||||
*
|
||||
* @param config the configuration to be set.
|
||||
*/
|
||||
public abstract void setReportingConfig(OrcaReportingConfig config);
|
||||
|
||||
/**
|
||||
* Returns a wrapped {@link LoadBalancer.Helper}. Subchannels created through it will retrieve
|
||||
* ORCA load reports if the server supports it.
|
||||
*/
|
||||
public abstract LoadBalancer.Helper asHelper();
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link OrcaReportingHelper} wraps a delegated {@link LoadBalancer.Helper} with additional
|
||||
* functionality to manage RPCs for out-of-band ORCA reporting for each backend it establishes
|
||||
* connection to.
|
||||
*/
|
||||
private static final class OrcaReportingHelper extends ForwardingLoadBalancerHelper
|
||||
implements OrcaOobReportListener {
|
||||
|
||||
private static final CreateSubchannelArgs.Key<OrcaReportingState> ORCA_REPORTING_STATE_KEY =
|
||||
CreateSubchannelArgs.Key.create("internal-orca-reporting-state");
|
||||
private final LoadBalancer.Helper delegate;
|
||||
private final OrcaOobReportListener listener;
|
||||
private final SynchronizationContext syncContext;
|
||||
private final BackoffPolicy.Provider backoffPolicyProvider;
|
||||
private final Supplier<Stopwatch> stopwatchSupplier;
|
||||
private final Set<OrcaReportingState> orcaStates = new HashSet<>();
|
||||
@Nullable private OrcaReportingConfig orcaConfig;
|
||||
|
||||
OrcaReportingHelper(
|
||||
LoadBalancer.Helper delegate,
|
||||
OrcaOobReportListener listener,
|
||||
BackoffPolicy.Provider backoffPolicyProvider,
|
||||
Supplier<Stopwatch> stopwatchSupplier) {
|
||||
this.delegate = checkNotNull(delegate, "delegate");
|
||||
this.listener = checkNotNull(listener, "listener");
|
||||
this.backoffPolicyProvider = checkNotNull(backoffPolicyProvider, "backoffPolicyProvider");
|
||||
this.stopwatchSupplier = checkNotNull(stopwatchSupplier, "stopwatchSupplier");
|
||||
syncContext = checkNotNull(delegate.getSynchronizationContext(), "syncContext");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Helper delegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Subchannel createSubchannel(CreateSubchannelArgs args) {
|
||||
syncContext.throwIfNotInThisSynchronizationContext();
|
||||
OrcaReportingState orcaState = args.getOption(ORCA_REPORTING_STATE_KEY);
|
||||
boolean augmented = false;
|
||||
if (orcaState == null) {
|
||||
// Only the first load balancing policy requesting ORCA reports instantiates an
|
||||
// OrcaReportingState.
|
||||
orcaState = new OrcaReportingState(this, syncContext,
|
||||
delegate().getScheduledExecutorService());
|
||||
args = args.toBuilder().addOption(ORCA_REPORTING_STATE_KEY, orcaState).build();
|
||||
augmented = true;
|
||||
}
|
||||
orcaStates.add(orcaState);
|
||||
orcaState.listeners.add(this);
|
||||
Subchannel subchannel = super.createSubchannel(args);
|
||||
if (augmented) {
|
||||
subchannel = new SubchannelImpl(subchannel, orcaState);
|
||||
}
|
||||
if (orcaConfig != null) {
|
||||
orcaState.setReportingConfig(this, orcaConfig);
|
||||
}
|
||||
return subchannel;
|
||||
}
|
||||
|
||||
void setReportingConfig(final OrcaReportingConfig config) {
|
||||
syncContext.throwIfNotInThisSynchronizationContext();
|
||||
orcaConfig = config;
|
||||
for (OrcaReportingState state : orcaStates) {
|
||||
state.setReportingConfig(OrcaReportingHelper.this, config);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadReport(OrcaLoadReport report) {
|
||||
syncContext.throwIfNotInThisSynchronizationContext();
|
||||
if (orcaConfig != null) {
|
||||
listener.onLoadReport(report);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link OrcaReportingState} is a client of ORCA service running on a single backend.
|
||||
*
|
||||
* <p>All methods are run from {@code syncContext}.
|
||||
*/
|
||||
private final class OrcaReportingState implements SubchannelStateListener {
|
||||
|
||||
private final OrcaReportingHelper orcaHelper;
|
||||
private final SynchronizationContext syncContext;
|
||||
private final ScheduledExecutorService timeService;
|
||||
private final List<OrcaOobReportListener> listeners = new ArrayList<>();
|
||||
private final Map<OrcaReportingHelper, OrcaReportingConfig> configs = new HashMap<>();
|
||||
@Nullable private Subchannel subchannel;
|
||||
@Nullable private ChannelLogger subchannelLogger;
|
||||
@Nullable
|
||||
private SubchannelStateListener stateListener;
|
||||
@Nullable private BackoffPolicy backoffPolicy;
|
||||
@Nullable private OrcaReportingStream orcaRpc;
|
||||
@Nullable private ScheduledHandle retryTimer;
|
||||
@Nullable private OrcaReportingConfig overallConfig;
|
||||
private final Runnable retryTask =
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
startRpc();
|
||||
}
|
||||
};
|
||||
private ConnectivityStateInfo state = ConnectivityStateInfo.forNonError(IDLE);
|
||||
// True if server returned UNIMPLEMENTED.
|
||||
private boolean disabled;
|
||||
|
||||
OrcaReportingState(
|
||||
OrcaReportingHelper orcaHelper,
|
||||
SynchronizationContext syncContext,
|
||||
ScheduledExecutorService timeService) {
|
||||
this.orcaHelper = checkNotNull(orcaHelper, "orcaHelper");
|
||||
this.syncContext = checkNotNull(syncContext, "syncContext");
|
||||
this.timeService = checkNotNull(timeService, "timeService");
|
||||
}
|
||||
|
||||
void init(Subchannel subchannel, SubchannelStateListener stateListener) {
|
||||
checkState(this.subchannel == null, "init() already called");
|
||||
this.subchannel = checkNotNull(subchannel, "subchannel");
|
||||
this.subchannelLogger = checkNotNull(subchannel.getChannelLogger(), "subchannelLogger");
|
||||
this.stateListener = checkNotNull(stateListener, "stateListener");
|
||||
}
|
||||
|
||||
void setReportingConfig(OrcaReportingHelper helper, OrcaReportingConfig config) {
|
||||
boolean reconfigured = false;
|
||||
configs.put(helper, config);
|
||||
// Real reporting interval is the minimum of intervals requested by all participating
|
||||
// helpers.
|
||||
if (overallConfig == null) {
|
||||
overallConfig = config.toBuilder().build();
|
||||
reconfigured = true;
|
||||
} else {
|
||||
long minInterval = Long.MAX_VALUE;
|
||||
for (OrcaReportingConfig c : configs.values()) {
|
||||
if (c.getReportIntervalNanos() < minInterval) {
|
||||
minInterval = c.getReportIntervalNanos();
|
||||
}
|
||||
}
|
||||
if (overallConfig.getReportIntervalNanos() != minInterval) {
|
||||
overallConfig = overallConfig.toBuilder()
|
||||
.setReportInterval(minInterval, TimeUnit.NANOSECONDS).build();
|
||||
reconfigured = true;
|
||||
}
|
||||
}
|
||||
if (reconfigured) {
|
||||
stopRpc("ORCA reporting reconfigured");
|
||||
adjustOrcaReporting();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubchannelState(ConnectivityStateInfo newState) {
|
||||
if (Objects.equal(state.getState(), READY) && !Objects.equal(newState.getState(), READY)) {
|
||||
// A connection was lost. We will reset disabled flag because ORCA service
|
||||
// may be available on the new connection.
|
||||
disabled = false;
|
||||
}
|
||||
if (Objects.equal(newState.getState(), SHUTDOWN)) {
|
||||
orcaHelper.orcaStates.remove(this);
|
||||
}
|
||||
state = newState;
|
||||
adjustOrcaReporting();
|
||||
// Propagate subchannel state update to downstream listeners.
|
||||
stateListener.onSubchannelState(newState);
|
||||
}
|
||||
|
||||
void adjustOrcaReporting() {
|
||||
if (!disabled && overallConfig != null && Objects.equal(state.getState(), READY)) {
|
||||
if (orcaRpc == null && !isRetryTimerPending()) {
|
||||
startRpc();
|
||||
}
|
||||
} else {
|
||||
stopRpc("Client stops ORCA reporting");
|
||||
backoffPolicy = null;
|
||||
}
|
||||
}
|
||||
|
||||
void startRpc() {
|
||||
checkState(orcaRpc == null, "previous orca reporting RPC has not been cleaned up");
|
||||
checkState(subchannel != null, "init() not called");
|
||||
subchannelLogger.log(
|
||||
ChannelLogLevel.DEBUG, "Starting ORCA reporting for {0}", subchannel.getAllAddresses());
|
||||
orcaRpc = new OrcaReportingStream(subchannel.asChannel(), stopwatchSupplier.get());
|
||||
orcaRpc.start();
|
||||
}
|
||||
|
||||
void stopRpc(String msg) {
|
||||
if (orcaRpc != null) {
|
||||
orcaRpc.cancel(msg);
|
||||
orcaRpc = null;
|
||||
}
|
||||
if (retryTimer != null) {
|
||||
retryTimer.cancel();
|
||||
retryTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
boolean isRetryTimerPending() {
|
||||
return retryTimer != null && retryTimer.isPending();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("disabled", disabled)
|
||||
.add("orcaRpc", orcaRpc)
|
||||
.add("reportingConfig", overallConfig)
|
||||
.add("connectivityState", state)
|
||||
.toString();
|
||||
}
|
||||
|
||||
private class OrcaReportingStream extends ClientCall.Listener<OrcaLoadReport> {
|
||||
|
||||
private final ClientCall<OrcaLoadReportRequest, OrcaLoadReport> call;
|
||||
private final Stopwatch stopwatch;
|
||||
private boolean callHasResponded;
|
||||
|
||||
OrcaReportingStream(Channel channel, Stopwatch stopwatch) {
|
||||
call =
|
||||
checkNotNull(channel, "channel")
|
||||
.newCall(OpenRcaServiceGrpc.getStreamCoreMetricsMethod(), CallOptions.DEFAULT);
|
||||
this.stopwatch = checkNotNull(stopwatch, "stopwatch");
|
||||
}
|
||||
|
||||
void start() {
|
||||
stopwatch.reset().start();
|
||||
call.start(this, new Metadata());
|
||||
call.sendMessage(
|
||||
OrcaLoadReportRequest.newBuilder()
|
||||
.setReportInterval(Durations.fromNanos(overallConfig.getReportIntervalNanos()))
|
||||
.build());
|
||||
call.halfClose();
|
||||
call.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(final OrcaLoadReport response) {
|
||||
syncContext.execute(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (orcaRpc == OrcaReportingStream.this) {
|
||||
handleResponse(response);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose(final Status status, Metadata trailers) {
|
||||
syncContext.execute(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (orcaRpc == OrcaReportingStream.this) {
|
||||
orcaRpc = null;
|
||||
handleStreamClosed(status);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void handleResponse(OrcaLoadReport response) {
|
||||
callHasResponded = true;
|
||||
backoffPolicy = null;
|
||||
subchannelLogger.log(ChannelLogLevel.DEBUG, "Received an ORCA report: {0}", response);
|
||||
for (OrcaOobReportListener listener : listeners) {
|
||||
listener.onLoadReport(response);
|
||||
}
|
||||
call.request(1);
|
||||
}
|
||||
|
||||
void handleStreamClosed(Status status) {
|
||||
if (Objects.equal(status.getCode(), Code.UNIMPLEMENTED)) {
|
||||
disabled = true;
|
||||
logger.log(
|
||||
Level.SEVERE,
|
||||
"Backend {0} OpenRcaService is disabled. Server returned: {1}",
|
||||
new Object[] {subchannel.getAllAddresses(), status});
|
||||
subchannelLogger.log(ChannelLogLevel.ERROR, "OpenRcaService disabled: {0}", status);
|
||||
return;
|
||||
}
|
||||
long delayNanos = 0;
|
||||
// Backoff only when no response has been received.
|
||||
if (!callHasResponded) {
|
||||
if (backoffPolicy == null) {
|
||||
backoffPolicy = backoffPolicyProvider.get();
|
||||
}
|
||||
delayNanos = backoffPolicy.nextBackoffNanos() - stopwatch.elapsed(TimeUnit.NANOSECONDS);
|
||||
}
|
||||
subchannelLogger.log(
|
||||
ChannelLogLevel.DEBUG,
|
||||
"ORCA reporting stream closed with {0}, backoff in {1} ns",
|
||||
status,
|
||||
delayNanos <= 0 ? 0 : delayNanos);
|
||||
if (delayNanos <= 0) {
|
||||
startRpc();
|
||||
} else {
|
||||
checkState(!isRetryTimerPending(), "Retry double scheduled");
|
||||
retryTimer =
|
||||
syncContext.schedule(retryTask, delayNanos, TimeUnit.NANOSECONDS, timeService);
|
||||
}
|
||||
}
|
||||
|
||||
void cancel(String msg) {
|
||||
call.cancel(msg, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("callStarted", call != null)
|
||||
.add("callHasResponded", callHasResponded)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static final class SubchannelImpl extends ForwardingSubchannel {
|
||||
|
||||
private final Subchannel delegate;
|
||||
private final OrcaReportingHelper.OrcaReportingState orcaState;
|
||||
|
||||
SubchannelImpl(Subchannel delegate, OrcaReportingHelper.OrcaReportingState orcaState) {
|
||||
this.delegate = checkNotNull(delegate, "delegate");
|
||||
this.orcaState = checkNotNull(orcaState, "orcaState");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Subchannel delegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(SubchannelStateListener listener) {
|
||||
orcaState.init(this, listener);
|
||||
super.start(orcaState);
|
||||
}
|
||||
}
|
||||
|
||||
/** Configuration for out-of-band ORCA reporting service RPC. */
|
||||
public static final class OrcaReportingConfig {
|
||||
|
||||
private final long reportIntervalNanos;
|
||||
|
||||
private OrcaReportingConfig(long reportIntervalNanos) {
|
||||
this.reportIntervalNanos = reportIntervalNanos;
|
||||
}
|
||||
|
||||
/** Creates a new builder. */
|
||||
public static Builder newBuilder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/** Returns the configured maximum interval of receiving out-of-band ORCA reports. */
|
||||
public long getReportIntervalNanos() {
|
||||
return reportIntervalNanos;
|
||||
}
|
||||
|
||||
/** Returns a builder with the same initial values as this object. */
|
||||
public Builder toBuilder() {
|
||||
return newBuilder().setReportInterval(reportIntervalNanos, TimeUnit.NANOSECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("reportIntervalNanos", reportIntervalNanos)
|
||||
.toString();
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private long reportIntervalNanos;
|
||||
|
||||
Builder() {}
|
||||
|
||||
/**
|
||||
* Sets the maximum expected interval of receiving out-of-band ORCA report. The actual
|
||||
* reporting interval might be smaller if there are other load balancing policies requesting
|
||||
* for more frequent cost metric report.
|
||||
*
|
||||
* @param reportInterval the maximum expected interval of receiving periodical ORCA reports.
|
||||
* @param unit time unit of {@code reportInterval} value.
|
||||
*/
|
||||
public Builder setReportInterval(long reportInterval, TimeUnit unit) {
|
||||
reportIntervalNanos = unit.toNanos(reportInterval);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Creates a new {@link OrcaReportingConfig} object. */
|
||||
public OrcaReportingConfig build() {
|
||||
return new OrcaReportingConfig(reportIntervalNanos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,270 @@
|
|||
/*
|
||||
* Copyright 2019 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.xds;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.envoyproxy.udpa.data.orca.v1.OrcaLoadReport;
|
||||
import io.grpc.CallOptions;
|
||||
import io.grpc.ClientStreamTracer;
|
||||
import io.grpc.ClientStreamTracer.StreamInfo;
|
||||
import io.grpc.ExperimentalApi;
|
||||
import io.grpc.LoadBalancer;
|
||||
import io.grpc.Metadata;
|
||||
import io.grpc.protobuf.ProtoUtils;
|
||||
import io.grpc.util.ForwardingClientStreamTracer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utility class that provides method for {@link LoadBalancer} to install listeners to receive
|
||||
* per-request backend cost metrics in the format of Open Request Cost Aggregation (ORCA).
|
||||
*/
|
||||
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/5790")
|
||||
public abstract class OrcaPerRequestUtil {
|
||||
private static final ClientStreamTracer NOOP_CLIENT_STREAM_TRACER = new ClientStreamTracer() {};
|
||||
private static final ClientStreamTracer.Factory NOOP_CLIENT_STREAM_TRACER_FACTORY =
|
||||
new ClientStreamTracer.Factory() {
|
||||
@Override
|
||||
public ClientStreamTracer newClientStreamTracer(StreamInfo info, Metadata headers) {
|
||||
return NOOP_CLIENT_STREAM_TRACER;
|
||||
}
|
||||
};
|
||||
private static final OrcaPerRequestUtil DEFAULT_INSTANCE =
|
||||
new OrcaPerRequestUtil() {
|
||||
@Override
|
||||
public ClientStreamTracer.Factory newOrcaClientStreamTracerFactory(
|
||||
OrcaPerRequestReportListener listener) {
|
||||
return newOrcaClientStreamTracerFactory(NOOP_CLIENT_STREAM_TRACER_FACTORY, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientStreamTracer.Factory newOrcaClientStreamTracerFactory(
|
||||
ClientStreamTracer.Factory delegate, OrcaPerRequestReportListener listener) {
|
||||
return new OrcaReportingTracerFactory(delegate, listener);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets an {@code OrcaPerRequestUtil} instance that provides actual implementation of
|
||||
* {@link #newOrcaClientStreamTracerFactory}.
|
||||
*/
|
||||
public static OrcaPerRequestUtil getInstance() {
|
||||
return DEFAULT_INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ClientStreamTracer.Factory} with provided {@link
|
||||
* OrcaPerRequestReportListener} installed to receive callback when a per-request ORCA report is
|
||||
* received.
|
||||
*
|
||||
* <p>Example usages for leaf level policy (e.g., WRR policy)
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
* class WrrPicker extends SubchannelPicker {
|
||||
*
|
||||
* public PickResult pickSubchannel(PickSubchannelArgs args) {
|
||||
* Subchannel subchannel = ... // WRR picking logic
|
||||
* return PickResult.withSubchannel(
|
||||
* subchannel,
|
||||
* OrcaPerRequestReportUtil.getInstance().newOrcaClientStreamTracerFactory(listener));
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @param listener contains the callback to be invoked when a per-request ORCA report is received.
|
||||
*/
|
||||
public abstract ClientStreamTracer.Factory newOrcaClientStreamTracerFactory(
|
||||
OrcaPerRequestReportListener listener);
|
||||
|
||||
/**
|
||||
* Creates a new {@link ClientStreamTracer.Factory} with provided {@link
|
||||
* OrcaPerRequestReportListener} installed to receive callback when a per-request ORCA report is
|
||||
* received.
|
||||
*
|
||||
* <p>Example usages:
|
||||
*
|
||||
* <ul>
|
||||
* <li> Delegating policy (e.g., xDS)
|
||||
* <pre>
|
||||
* {@code
|
||||
* class XdsPicker extends SubchannelPicker {
|
||||
*
|
||||
* public PickResult pickSubchannel(PickSubchannelArgs args) {
|
||||
* SubchannelPicker perLocalityPicker = ... // locality picking logic
|
||||
* Result result = perLocalityPicker.pickSubchannel(args);
|
||||
* return PickResult.withSubchannel(
|
||||
* result.getSubchannel(),
|
||||
* OrcaPerRequestReportUtil.getInstance().newOrcaClientTracerFactory(
|
||||
* result.getStreamTracerFactory(), listener));
|
||||
*
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* </li>
|
||||
* <li> Delegating policy with additional tracing logic
|
||||
* <pre>
|
||||
* {@code
|
||||
* class WrappingPicker extends SubchannelPicker {
|
||||
*
|
||||
* public PickResult pickSubchannel(PickSubchannelArgs args) {
|
||||
* Result result = delegate.pickSubchannel(args);
|
||||
* return PickResult.withSubchannel(
|
||||
* result.getSubchannel(),
|
||||
* new ClientStreamTracer.Factory() {
|
||||
* public ClientStreamTracer newClientStreamTracer(
|
||||
* StreamInfo info, Metadata metadata) {
|
||||
* ClientStreamTracer.Factory orcaTracerFactory =
|
||||
* OrcaPerRequestReportUtil.getInstance().newOrcaClientStreamTracerFactory(
|
||||
* result.getStreamTracerFactory(), listener);
|
||||
*
|
||||
* // Wrap the tracer from the delegate factory if you need to trace the
|
||||
* // stream for your own.
|
||||
* final ClientStreamTracer orcaTracer =
|
||||
* orcaTracerFactory.newClientStreamTracer(info, metadata);
|
||||
*
|
||||
* return ForwardingClientStreamTracer() {
|
||||
* protected ClientStreamTracer delegate() {
|
||||
* return orcaTracer;
|
||||
* }
|
||||
*
|
||||
* public void inboundMessage(int seqNo) {
|
||||
* // Handle this event.
|
||||
* ...
|
||||
* }
|
||||
* };
|
||||
* }
|
||||
* });
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
* @param delegate the delegate factory to produce other client stream tracing.
|
||||
* @param listener contains the callback to be invoked when a per-request ORCA report is received.
|
||||
*/
|
||||
public abstract ClientStreamTracer.Factory newOrcaClientStreamTracerFactory(
|
||||
ClientStreamTracer.Factory delegate, OrcaPerRequestReportListener listener);
|
||||
|
||||
/**
|
||||
* The listener interface for receiving per-request ORCA reports from backends. The class that is
|
||||
* interested in processing backend cost metrics implements this interface, and the object created
|
||||
* with that class is registered with a component, using methods in {@link OrcaPerRequestUtil}.
|
||||
* When an ORCA report is received, that object's {@code onLoadReport} method is invoked.
|
||||
*/
|
||||
public interface OrcaPerRequestReportListener {
|
||||
|
||||
/**
|
||||
* Invoked when an per-request ORCA report is received.
|
||||
*
|
||||
* <p>Note this callback will be invoked from the network thread as the RPC finishes,
|
||||
* implementations should not block.
|
||||
*
|
||||
* @param report load report in the format of ORCA format.
|
||||
*/
|
||||
void onLoadReport(OrcaLoadReport report);
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link OrcaReportingTracerFactory} wraps a delegated {@link ClientStreamTracer.Factory} with
|
||||
* additional functionality to produce {@link ClientStreamTracer} instances that extract
|
||||
* per-request ORCA reports and push to registered listeners for calls they trace.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final class OrcaReportingTracerFactory extends ClientStreamTracer.Factory {
|
||||
|
||||
@VisibleForTesting
|
||||
static final Metadata.Key<OrcaLoadReport> ORCA_ENDPOINT_LOAD_METRICS_KEY =
|
||||
Metadata.Key.of(
|
||||
"x-endpoint-load-metrics-bin",
|
||||
ProtoUtils.metadataMarshaller(OrcaLoadReport.getDefaultInstance()));
|
||||
|
||||
private static final CallOptions.Key<OrcaReportBroker> ORCA_REPORT_BROKER_KEY =
|
||||
CallOptions.Key.create("internal-orca-report-broker");
|
||||
private final ClientStreamTracer.Factory delegate;
|
||||
private final OrcaPerRequestReportListener listener;
|
||||
|
||||
OrcaReportingTracerFactory(
|
||||
ClientStreamTracer.Factory delegate, OrcaPerRequestReportListener listener) {
|
||||
this.delegate = checkNotNull(delegate, "delegate");
|
||||
this.listener = checkNotNull(listener, "listener");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientStreamTracer newClientStreamTracer(StreamInfo info, Metadata headers) {
|
||||
OrcaReportBroker broker = info.getCallOptions().getOption(ORCA_REPORT_BROKER_KEY);
|
||||
boolean augmented = false;
|
||||
if (broker == null) {
|
||||
broker = new OrcaReportBroker();
|
||||
info =
|
||||
info.toBuilder()
|
||||
.setCallOptions(info.getCallOptions().withOption(ORCA_REPORT_BROKER_KEY, broker))
|
||||
.build();
|
||||
augmented = true;
|
||||
}
|
||||
broker.addListener(listener);
|
||||
ClientStreamTracer tracer = delegate.newClientStreamTracer(info, headers);
|
||||
if (augmented) {
|
||||
final ClientStreamTracer currTracer = tracer;
|
||||
final OrcaReportBroker currBroker = broker;
|
||||
// The actual tracer that performs ORCA report deserialization.
|
||||
tracer =
|
||||
new ForwardingClientStreamTracer() {
|
||||
@Override
|
||||
protected ClientStreamTracer delegate() {
|
||||
return currTracer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inboundTrailers(Metadata trailers) {
|
||||
OrcaLoadReport report = trailers.get(ORCA_ENDPOINT_LOAD_METRICS_KEY);
|
||||
if (report != null) {
|
||||
currBroker.onReport(report);
|
||||
}
|
||||
delegate().inboundTrailers(trailers);
|
||||
}
|
||||
};
|
||||
}
|
||||
return tracer;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A container class to hold registered {@link OrcaPerRequestReportListener}s and invoke all of
|
||||
* them when an {@link OrcaLoadReport} is received.
|
||||
*/
|
||||
private static final class OrcaReportBroker {
|
||||
|
||||
private final List<OrcaPerRequestReportListener> listeners = new ArrayList<>();
|
||||
|
||||
void addListener(OrcaPerRequestReportListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
void onReport(OrcaLoadReport report) {
|
||||
for (OrcaPerRequestReportListener listener : listeners) {
|
||||
listener.onLoadReport(report);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,892 @@
|
|||
/*
|
||||
* Copyright 2019 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.xds;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
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 org.mockito.AdditionalAnswers.delegatesTo;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.atLeast;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.google.protobuf.util.Durations;
|
||||
import io.envoyproxy.udpa.data.orca.v1.OrcaLoadReport;
|
||||
import io.envoyproxy.udpa.service.orca.v1.OpenRcaServiceGrpc;
|
||||
import io.envoyproxy.udpa.service.orca.v1.OrcaLoadReportRequest;
|
||||
import io.grpc.Attributes;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.ChannelLogger;
|
||||
import io.grpc.ConnectivityState;
|
||||
import io.grpc.ConnectivityStateInfo;
|
||||
import io.grpc.Context;
|
||||
import io.grpc.Context.CancellationListener;
|
||||
import io.grpc.EquivalentAddressGroup;
|
||||
import io.grpc.LoadBalancer.CreateSubchannelArgs;
|
||||
import io.grpc.LoadBalancer.Helper;
|
||||
import io.grpc.LoadBalancer.Subchannel;
|
||||
import io.grpc.LoadBalancer.SubchannelPicker;
|
||||
import io.grpc.LoadBalancer.SubchannelStateListener;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.NameResolver;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.SynchronizationContext;
|
||||
import io.grpc.inprocess.InProcessChannelBuilder;
|
||||
import io.grpc.inprocess.InProcessServerBuilder;
|
||||
import io.grpc.internal.BackoffPolicy;
|
||||
import io.grpc.internal.FakeClock;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import io.grpc.testing.GrpcCleanupRule;
|
||||
import io.grpc.xds.OrcaOobUtil.OrcaOobReportListener;
|
||||
import io.grpc.xds.OrcaOobUtil.OrcaReportingConfig;
|
||||
import io.grpc.xds.OrcaOobUtil.OrcaReportingHelperWrapper;
|
||||
import io.grpc.xds.OrcaOobUtil.SubchannelImpl;
|
||||
import java.net.SocketAddress;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link OrcaOobUtil} class.
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
public class OrcaOobUtilTest {
|
||||
|
||||
private static final int NUM_SUBCHANNELS = 2;
|
||||
private static final Attributes.Key<String> SUBCHANNEL_ATTR_KEY =
|
||||
Attributes.Key.create("subchannel-attr-for-test");
|
||||
private static final OrcaReportingConfig SHORT_INTERVAL_CONFIG =
|
||||
OrcaReportingConfig.newBuilder().setReportInterval(5L, TimeUnit.NANOSECONDS).build();
|
||||
private static final OrcaReportingConfig MEDIUM_INTERVAL_CONFIG =
|
||||
OrcaReportingConfig.newBuilder().setReportInterval(543L, TimeUnit.MICROSECONDS).build();
|
||||
private static final OrcaReportingConfig LONG_INTERVAL_CONFIG =
|
||||
OrcaReportingConfig.newBuilder().setReportInterval(1232L, TimeUnit.MILLISECONDS).build();
|
||||
@Rule public final GrpcCleanupRule cleanupRule = new GrpcCleanupRule();
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private final List<EquivalentAddressGroup>[] eagLists = new List[NUM_SUBCHANNELS];
|
||||
private final SubchannelStateListener[] mockStateListeners =
|
||||
new SubchannelStateListener[NUM_SUBCHANNELS];
|
||||
private final ManagedChannel[] channels = new ManagedChannel[NUM_SUBCHANNELS];
|
||||
private final OpenRcaServiceImp[] orcaServiceImps = new OpenRcaServiceImp[NUM_SUBCHANNELS];
|
||||
private final SynchronizationContext syncContext = new SynchronizationContext(
|
||||
new Thread.UncaughtExceptionHandler() {
|
||||
@Override
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
});
|
||||
|
||||
private final FakeClock fakeClock = new FakeClock();
|
||||
private final Helper origHelper = mock(Helper.class, delegatesTo(new FakeHelper()));
|
||||
@Mock
|
||||
private OrcaOobReportListener mockOrcaListener0;
|
||||
@Mock
|
||||
private OrcaOobReportListener mockOrcaListener1;
|
||||
@Mock
|
||||
private OrcaOobReportListener mockOrcaListener2;
|
||||
@Mock private BackoffPolicy.Provider backoffPolicyProvider;
|
||||
@Mock private BackoffPolicy backoffPolicy1;
|
||||
@Mock private BackoffPolicy backoffPolicy2;
|
||||
private FakeSubchannel[] subchannels = new FakeSubchannel[NUM_SUBCHANNELS];
|
||||
private OrcaReportingHelperWrapper orcaHelperWrapper;
|
||||
private OrcaReportingHelperWrapper parentHelperWrapper;
|
||||
private OrcaReportingHelperWrapper childHelperWrapper;
|
||||
|
||||
private static FakeSubchannel unwrap(Subchannel s) {
|
||||
return (FakeSubchannel) ((SubchannelImpl) s).delegate();
|
||||
}
|
||||
|
||||
private static OrcaLoadReportRequest buildOrcaRequestFromConfig(
|
||||
OrcaReportingConfig config) {
|
||||
return OrcaLoadReportRequest.newBuilder()
|
||||
.setReportInterval(Durations.fromNanos(config.getReportIntervalNanos()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static void assertLog(List<String> logs, String expectedLog) {
|
||||
assertThat(logs).containsExactly(expectedLog);
|
||||
logs.clear();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
for (int i = 0; i < NUM_SUBCHANNELS; i++) {
|
||||
if (subchannels[i] != null) {
|
||||
subchannels[i].shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void orcaReportingConfig_construct() {
|
||||
int interval = new Random().nextInt(Integer.MAX_VALUE);
|
||||
OrcaReportingConfig config =
|
||||
OrcaReportingConfig.newBuilder()
|
||||
.setReportInterval(interval, TimeUnit.MICROSECONDS)
|
||||
.build();
|
||||
assertThat(config.getReportIntervalNanos()).isEqualTo(TimeUnit.MICROSECONDS.toNanos(interval));
|
||||
String str = config.toString();
|
||||
assertThat(str).contains("reportIntervalNanos=");
|
||||
OrcaReportingConfig rebuildedConfig = config.toBuilder().build();
|
||||
assertThat(rebuildedConfig.getReportIntervalNanos())
|
||||
.isEqualTo(TimeUnit.MICROSECONDS.toNanos(interval));
|
||||
}
|
||||
|
||||
@Before
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setUp() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
for (int i = 0; i < NUM_SUBCHANNELS; i++) {
|
||||
orcaServiceImps[i] = new OpenRcaServiceImp();
|
||||
cleanupRule.register(
|
||||
InProcessServerBuilder.forName("orca-reporting-test-" + i)
|
||||
.addService(orcaServiceImps[i])
|
||||
.directExecutor()
|
||||
.build()
|
||||
.start());
|
||||
ManagedChannel channel =
|
||||
cleanupRule.register(
|
||||
InProcessChannelBuilder.forName("orca-reporting-test-" + i).directExecutor().build());
|
||||
channels[i] = channel;
|
||||
EquivalentAddressGroup eag =
|
||||
new EquivalentAddressGroup(new FakeSocketAddress("address-" + i));
|
||||
List<EquivalentAddressGroup> eagList = Arrays.asList(eag);
|
||||
eagLists[i] = eagList;
|
||||
mockStateListeners[i] = mock(SubchannelStateListener.class);
|
||||
}
|
||||
|
||||
when(backoffPolicyProvider.get()).thenReturn(backoffPolicy1, backoffPolicy2);
|
||||
when(backoffPolicy1.nextBackoffNanos()).thenReturn(11L, 21L);
|
||||
when(backoffPolicy2.nextBackoffNanos()).thenReturn(12L, 22L);
|
||||
|
||||
orcaHelperWrapper =
|
||||
OrcaOobUtil.newOrcaReportingHelperWrapper(
|
||||
origHelper,
|
||||
mockOrcaListener0,
|
||||
backoffPolicyProvider,
|
||||
fakeClock.getStopwatchSupplier());
|
||||
parentHelperWrapper =
|
||||
OrcaOobUtil.newOrcaReportingHelperWrapper(
|
||||
origHelper,
|
||||
mockOrcaListener1,
|
||||
backoffPolicyProvider,
|
||||
fakeClock.getStopwatchSupplier());
|
||||
childHelperWrapper =
|
||||
OrcaOobUtil.newOrcaReportingHelperWrapper(
|
||||
parentHelperWrapper.asHelper(),
|
||||
mockOrcaListener2,
|
||||
backoffPolicyProvider,
|
||||
fakeClock.getStopwatchSupplier());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void singlePolicyTypicalWorkflow() {
|
||||
setOrcaReportConfig(orcaHelperWrapper, SHORT_INTERVAL_CONFIG);
|
||||
verify(origHelper, atLeast(0)).getSynchronizationContext();
|
||||
verifyNoMoreInteractions(origHelper);
|
||||
|
||||
// Calling createSubchannel() on orcaHelper correctly passes augmented CreateSubchannelArgs
|
||||
// to origHelper.
|
||||
ArgumentCaptor<CreateSubchannelArgs> createArgsCaptor = ArgumentCaptor.forClass(null);
|
||||
for (int i = 0; i < NUM_SUBCHANNELS; i++) {
|
||||
String subchannelAttrValue = "eag attr " + i;
|
||||
Attributes attrs =
|
||||
Attributes.newBuilder().set(SUBCHANNEL_ATTR_KEY, subchannelAttrValue).build();
|
||||
assertThat(unwrap(createSubchannel(orcaHelperWrapper.asHelper(), i, attrs)))
|
||||
.isSameInstanceAs(subchannels[i]);
|
||||
verify(origHelper, times(i + 1)).createSubchannel(createArgsCaptor.capture());
|
||||
assertThat(createArgsCaptor.getValue().getAddresses()).isEqualTo(eagLists[i]);
|
||||
assertThat(createArgsCaptor.getValue().getAttributes().get(SUBCHANNEL_ATTR_KEY))
|
||||
.isEqualTo(subchannelAttrValue);
|
||||
}
|
||||
|
||||
// ORCA reporting does not start until underlying Subchannel is READY.
|
||||
for (int i = 0; i < NUM_SUBCHANNELS; i++) {
|
||||
FakeSubchannel subchannel = subchannels[i];
|
||||
OpenRcaServiceImp orcaServiceImp = orcaServiceImps[i];
|
||||
SubchannelStateListener mockStateListener = mockStateListeners[i];
|
||||
InOrder inOrder = inOrder(mockStateListener);
|
||||
deliverSubchannelState(i, ConnectivityStateInfo.forNonError(IDLE));
|
||||
deliverSubchannelState(i, ConnectivityStateInfo.forTransientFailure(Status.UNAVAILABLE));
|
||||
deliverSubchannelState(i, ConnectivityStateInfo.forNonError(CONNECTING));
|
||||
|
||||
inOrder.verify(mockStateListener)
|
||||
.onSubchannelState(eq(ConnectivityStateInfo.forNonError(IDLE)));
|
||||
inOrder.verify(mockStateListener)
|
||||
.onSubchannelState(eq(ConnectivityStateInfo.forTransientFailure(Status.UNAVAILABLE)));
|
||||
inOrder.verify(mockStateListener)
|
||||
.onSubchannelState(eq(ConnectivityStateInfo.forNonError(CONNECTING)));
|
||||
verifyNoMoreInteractions(mockStateListener);
|
||||
|
||||
assertThat(subchannel.logs).isEmpty();
|
||||
assertThat(orcaServiceImp.calls).isEmpty();
|
||||
verifyNoMoreInteractions(mockOrcaListener0);
|
||||
deliverSubchannelState(i, ConnectivityStateInfo.forNonError(READY));
|
||||
verify(mockStateListener).onSubchannelState(eq(ConnectivityStateInfo.forNonError(READY)));
|
||||
assertThat(orcaServiceImp.calls).hasSize(1);
|
||||
ServerSideCall serverCall = orcaServiceImp.calls.peek();
|
||||
assertThat(serverCall.request).isEqualTo(buildOrcaRequestFromConfig(SHORT_INTERVAL_CONFIG));
|
||||
assertLog(subchannel.logs,
|
||||
"DEBUG: Starting ORCA reporting for " + subchannel.getAllAddresses());
|
||||
|
||||
// Simulate an ORCA service response. Registered listener will receive an ORCA report for
|
||||
// each backend.
|
||||
OrcaLoadReport report = OrcaLoadReport.getDefaultInstance();
|
||||
serverCall.responseObserver.onNext(report);
|
||||
assertLog(subchannel.logs, "DEBUG: Received an ORCA report: " + report);
|
||||
verify(mockOrcaListener0, times(i + 1)).onLoadReport(eq(report));
|
||||
}
|
||||
|
||||
for (int i = 0; i < NUM_SUBCHANNELS; i++) {
|
||||
FakeSubchannel subchannel = subchannels[i];
|
||||
SubchannelStateListener mockStateListener = mockStateListeners[i];
|
||||
|
||||
ServerSideCall serverCall = orcaServiceImps[i].calls.peek();
|
||||
assertThat(serverCall.cancelled).isFalse();
|
||||
verifyNoMoreInteractions(mockStateListener);
|
||||
|
||||
// Shutting down the subchannel will cancel the ORCA reporting RPC.
|
||||
subchannel.shutdown();
|
||||
verify(mockStateListener).onSubchannelState(eq(ConnectivityStateInfo.forNonError(SHUTDOWN)));
|
||||
assertThat(serverCall.cancelled).isTrue();
|
||||
assertThat(subchannel.logs).isEmpty();
|
||||
verifyNoMoreInteractions(mockOrcaListener0);
|
||||
}
|
||||
|
||||
for (int i = 0; i < NUM_SUBCHANNELS; i++) {
|
||||
assertThat(orcaServiceImps[i].calls).hasSize(1);
|
||||
}
|
||||
|
||||
verifyZeroInteractions(backoffPolicyProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void twoLevelPoliciesTypicalWorkflow() {
|
||||
setOrcaReportConfig(childHelperWrapper, SHORT_INTERVAL_CONFIG);
|
||||
setOrcaReportConfig(parentHelperWrapper, SHORT_INTERVAL_CONFIG);
|
||||
verify(origHelper, atLeast(0)).getSynchronizationContext();
|
||||
verifyNoMoreInteractions(origHelper);
|
||||
|
||||
// Calling createSubchannel() on child helper correctly passes augmented CreateSubchannelArgs
|
||||
// to origHelper.
|
||||
ArgumentCaptor<CreateSubchannelArgs> createArgsCaptor = ArgumentCaptor.forClass(null);
|
||||
for (int i = 0; i < NUM_SUBCHANNELS; i++) {
|
||||
String subchannelAttrValue = "eag attr " + i;
|
||||
Attributes attrs =
|
||||
Attributes.newBuilder().set(SUBCHANNEL_ATTR_KEY, subchannelAttrValue).build();
|
||||
assertThat(unwrap(createSubchannel(childHelperWrapper.asHelper(), i, attrs)))
|
||||
.isSameInstanceAs(subchannels[i]);
|
||||
verify(origHelper, times(i + 1)).createSubchannel(createArgsCaptor.capture());
|
||||
assertThat(createArgsCaptor.getValue().getAddresses()).isEqualTo(eagLists[i]);
|
||||
assertThat(createArgsCaptor.getValue().getAttributes().get(SUBCHANNEL_ATTR_KEY))
|
||||
.isEqualTo(subchannelAttrValue);
|
||||
}
|
||||
|
||||
// ORCA reporting does not start until underlying Subchannel is READY.
|
||||
for (int i = 0; i < NUM_SUBCHANNELS; i++) {
|
||||
FakeSubchannel subchannel = subchannels[i];
|
||||
OpenRcaServiceImp orcaServiceImp = orcaServiceImps[i];
|
||||
SubchannelStateListener mockStateListener = mockStateListeners[i];
|
||||
InOrder inOrder = inOrder(mockStateListener);
|
||||
deliverSubchannelState(i, ConnectivityStateInfo.forNonError(IDLE));
|
||||
deliverSubchannelState(i, ConnectivityStateInfo.forTransientFailure(Status.UNAVAILABLE));
|
||||
deliverSubchannelState(i, ConnectivityStateInfo.forNonError(CONNECTING));
|
||||
|
||||
inOrder
|
||||
.verify(mockStateListener).onSubchannelState(eq(ConnectivityStateInfo.forNonError(IDLE)));
|
||||
inOrder
|
||||
.verify(mockStateListener)
|
||||
.onSubchannelState(eq(ConnectivityStateInfo.forTransientFailure(Status.UNAVAILABLE)));
|
||||
inOrder
|
||||
.verify(mockStateListener)
|
||||
.onSubchannelState(eq(ConnectivityStateInfo.forNonError(CONNECTING)));
|
||||
verifyNoMoreInteractions(mockStateListener);
|
||||
|
||||
assertThat(subchannel.logs).isEmpty();
|
||||
assertThat(orcaServiceImp.calls).isEmpty();
|
||||
verifyNoMoreInteractions(mockOrcaListener1);
|
||||
verifyNoMoreInteractions(mockOrcaListener2);
|
||||
deliverSubchannelState(i, ConnectivityStateInfo.forNonError(READY));
|
||||
verify(mockStateListener).onSubchannelState(eq(ConnectivityStateInfo.forNonError(READY)));
|
||||
assertThat(orcaServiceImp.calls).hasSize(1);
|
||||
ServerSideCall serverCall = orcaServiceImp.calls.peek();
|
||||
assertThat(serverCall.request).isEqualTo(buildOrcaRequestFromConfig(SHORT_INTERVAL_CONFIG));
|
||||
assertLog(subchannel.logs,
|
||||
"DEBUG: Starting ORCA reporting for " + subchannel.getAllAddresses());
|
||||
|
||||
// Simulate an ORCA service response. Registered listener will receive an ORCA report for
|
||||
// each backend.
|
||||
OrcaLoadReport report = OrcaLoadReport.getDefaultInstance();
|
||||
serverCall.responseObserver.onNext(report);
|
||||
assertLog(subchannel.logs, "DEBUG: Received an ORCA report: " + report);
|
||||
verify(mockOrcaListener1, times(i + 1)).onLoadReport(eq(report));
|
||||
verify(mockOrcaListener2, times(i + 1)).onLoadReport(eq(report));
|
||||
}
|
||||
|
||||
for (int i = 0; i < NUM_SUBCHANNELS; i++) {
|
||||
FakeSubchannel subchannel = subchannels[i];
|
||||
SubchannelStateListener mockStateListener = mockStateListeners[i];
|
||||
|
||||
ServerSideCall serverCall = orcaServiceImps[i].calls.peek();
|
||||
assertThat(serverCall.cancelled).isFalse();
|
||||
verifyNoMoreInteractions(mockStateListener);
|
||||
|
||||
// Shutting down the subchannel will cancel the ORCA reporting RPC.
|
||||
subchannel.shutdown();
|
||||
verify(mockStateListener).onSubchannelState(eq(ConnectivityStateInfo.forNonError(SHUTDOWN)));
|
||||
assertThat(serverCall.cancelled).isTrue();
|
||||
assertThat(subchannel.logs).isEmpty();
|
||||
verifyNoMoreInteractions(mockOrcaListener1, mockOrcaListener2);
|
||||
}
|
||||
|
||||
for (int i = 0; i < NUM_SUBCHANNELS; i++) {
|
||||
assertThat(orcaServiceImps[i].calls).hasSize(1);
|
||||
}
|
||||
|
||||
verifyZeroInteractions(backoffPolicyProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void orcReportingDisabledWhenServiceNotImplemented() {
|
||||
setOrcaReportConfig(orcaHelperWrapper, SHORT_INTERVAL_CONFIG);
|
||||
createSubchannel(orcaHelperWrapper.asHelper(), 0, Attributes.EMPTY);
|
||||
FakeSubchannel subchannel = subchannels[0];
|
||||
OpenRcaServiceImp orcaServiceImp = orcaServiceImps[0];
|
||||
SubchannelStateListener mockStateListener = mockStateListeners[0];
|
||||
deliverSubchannelState(0, ConnectivityStateInfo.forNonError(READY));
|
||||
verify(mockStateListener).onSubchannelState(eq(ConnectivityStateInfo.forNonError(READY)));
|
||||
assertThat(orcaServiceImp.calls).hasSize(1);
|
||||
|
||||
ServerSideCall serverCall = orcaServiceImp.calls.poll();
|
||||
assertThat(serverCall.request).isEqualTo(buildOrcaRequestFromConfig(SHORT_INTERVAL_CONFIG));
|
||||
subchannel.logs.clear();
|
||||
serverCall.responseObserver.onError(Status.UNIMPLEMENTED.asException());
|
||||
assertLog(subchannel.logs,
|
||||
"ERROR: OpenRcaService disabled: " + Status.UNIMPLEMENTED);
|
||||
verifyNoMoreInteractions(mockOrcaListener0);
|
||||
|
||||
// Re-connecting on Subchannel will reset the "disabled" flag and restart ORCA reporting.
|
||||
assertThat(orcaServiceImp.calls).hasSize(0);
|
||||
deliverSubchannelState(0, ConnectivityStateInfo.forNonError(IDLE));
|
||||
deliverSubchannelState(0, ConnectivityStateInfo.forNonError(READY));
|
||||
assertLog(subchannel.logs,
|
||||
"DEBUG: Starting ORCA reporting for " + subchannel.getAllAddresses());
|
||||
assertThat(orcaServiceImp.calls).hasSize(1);
|
||||
serverCall = orcaServiceImp.calls.poll();
|
||||
OrcaLoadReport report = OrcaLoadReport.getDefaultInstance();
|
||||
serverCall.responseObserver.onNext(report);
|
||||
assertLog(subchannel.logs, "DEBUG: Received an ORCA report: " + report);
|
||||
verify(mockOrcaListener0).onLoadReport(eq(report));
|
||||
|
||||
verifyZeroInteractions(backoffPolicyProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void orcaReportingStreamClosedAndRetried() {
|
||||
setOrcaReportConfig(orcaHelperWrapper, SHORT_INTERVAL_CONFIG);
|
||||
createSubchannel(orcaHelperWrapper.asHelper(), 0, Attributes.EMPTY);
|
||||
FakeSubchannel subchannel = subchannels[0];
|
||||
OpenRcaServiceImp orcaServiceImp = orcaServiceImps[0];
|
||||
SubchannelStateListener mockStateListener = mockStateListeners[0];
|
||||
InOrder inOrder = inOrder(mockStateListener, mockOrcaListener0, backoffPolicyProvider,
|
||||
backoffPolicy1, backoffPolicy2);
|
||||
|
||||
deliverSubchannelState(0, ConnectivityStateInfo.forNonError(READY));
|
||||
inOrder
|
||||
.verify(mockStateListener).onSubchannelState(eq(ConnectivityStateInfo.forNonError(READY)));
|
||||
assertLog(subchannel.logs,
|
||||
"DEBUG: Starting ORCA reporting for " + subchannel.getAllAddresses());
|
||||
|
||||
// Server closes the ORCA reporting RPC without any response, will start backoff
|
||||
// sequence 1 (11ns).
|
||||
orcaServiceImp.calls.poll().responseObserver.onCompleted();
|
||||
assertLog(subchannel.logs,
|
||||
"DEBUG: ORCA reporting stream closed with " + Status.OK + ", backoff in 11" + " ns");
|
||||
inOrder.verify(backoffPolicyProvider).get();
|
||||
inOrder.verify(backoffPolicy1).nextBackoffNanos();
|
||||
verifyRetryAfterNanos(inOrder, orcaServiceImp, 11);
|
||||
assertLog(subchannel.logs,
|
||||
"DEBUG: Starting ORCA reporting for " + subchannel.getAllAddresses());
|
||||
|
||||
// Server closes the ORCA reporting RPC with an error, will continue backoff sequence 1 (21ns).
|
||||
orcaServiceImp.calls.poll().responseObserver.onError(Status.UNAVAILABLE.asException());
|
||||
assertLog(subchannel.logs,
|
||||
"DEBUG: ORCA reporting stream closed with " + Status.UNAVAILABLE + ", backoff in 21"
|
||||
+ " ns");
|
||||
inOrder.verify(backoffPolicy1).nextBackoffNanos();
|
||||
verifyRetryAfterNanos(inOrder, orcaServiceImp, 21);
|
||||
assertLog(subchannel.logs,
|
||||
"DEBUG: Starting ORCA reporting for " + subchannel.getAllAddresses());
|
||||
|
||||
// Server responds normally.
|
||||
OrcaLoadReport report = OrcaLoadReport.getDefaultInstance();
|
||||
orcaServiceImp.calls.peek().responseObserver.onNext(report);
|
||||
assertLog(subchannel.logs, "DEBUG: Received an ORCA report: " + report);
|
||||
inOrder.verify(mockOrcaListener0).onLoadReport(eq(report));
|
||||
|
||||
// Server closes the ORCA reporting RPC after a response, will restart immediately.
|
||||
orcaServiceImp.calls.poll().responseObserver.onCompleted();
|
||||
assertThat(subchannel.logs).containsExactly(
|
||||
"DEBUG: ORCA reporting stream closed with " + Status.OK + ", backoff in 0" + " ns",
|
||||
"DEBUG: Starting ORCA reporting for " + subchannel.getAllAddresses());
|
||||
subchannel.logs.clear();
|
||||
|
||||
// Backoff policy is set to sequence 2 in previous retry.
|
||||
// Server closes the ORCA reporting RPC with an error, will start backoff sequence 2 (12ns).
|
||||
orcaServiceImp.calls.poll().responseObserver.onError(Status.UNAVAILABLE.asException());
|
||||
assertLog(subchannel.logs,
|
||||
"DEBUG: ORCA reporting stream closed with " + Status.UNAVAILABLE + ", backoff in 12"
|
||||
+ " ns");
|
||||
inOrder.verify(backoffPolicyProvider).get();
|
||||
inOrder.verify(backoffPolicy2).nextBackoffNanos();
|
||||
verifyRetryAfterNanos(inOrder, orcaServiceImp, 12);
|
||||
assertLog(subchannel.logs,
|
||||
"DEBUG: Starting ORCA reporting for " + subchannel.getAllAddresses());
|
||||
|
||||
verifyNoMoreInteractions(mockStateListener, mockOrcaListener0, backoffPolicyProvider,
|
||||
backoffPolicy1, backoffPolicy2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportingNotStartedUntilConfigured() {
|
||||
createSubchannel(orcaHelperWrapper.asHelper(), 0, Attributes.EMPTY);
|
||||
deliverSubchannelState(0, ConnectivityStateInfo.forNonError(READY));
|
||||
verify(mockStateListeners[0])
|
||||
.onSubchannelState(eq(ConnectivityStateInfo.forNonError(READY)));
|
||||
|
||||
assertThat(orcaServiceImps[0].calls).isEmpty();
|
||||
assertThat(subchannels[0].logs).isEmpty();
|
||||
setOrcaReportConfig(orcaHelperWrapper, SHORT_INTERVAL_CONFIG);
|
||||
assertThat(orcaServiceImps[0].calls).hasSize(1);
|
||||
assertLog(subchannels[0].logs,
|
||||
"DEBUG: Starting ORCA reporting for " + subchannels[0].getAllAddresses());
|
||||
assertThat(orcaServiceImps[0].calls.peek().request)
|
||||
.isEqualTo(buildOrcaRequestFromConfig(SHORT_INTERVAL_CONFIG));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateReportingIntervalBeforeCreatingSubchannel() {
|
||||
setOrcaReportConfig(orcaHelperWrapper, SHORT_INTERVAL_CONFIG);
|
||||
createSubchannel(orcaHelperWrapper.asHelper(), 0, Attributes.EMPTY);
|
||||
deliverSubchannelState(0, ConnectivityStateInfo.forNonError(READY));
|
||||
verify(mockStateListeners[0]).onSubchannelState(eq(ConnectivityStateInfo.forNonError(READY)));
|
||||
|
||||
assertThat(orcaServiceImps[0].calls).hasSize(1);
|
||||
assertLog(subchannels[0].logs,
|
||||
"DEBUG: Starting ORCA reporting for " + subchannels[0].getAllAddresses());
|
||||
assertThat(orcaServiceImps[0].calls.poll().request)
|
||||
.isEqualTo(buildOrcaRequestFromConfig(SHORT_INTERVAL_CONFIG));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateReportingIntervalBeforeSubchannelReady() {
|
||||
createSubchannel(orcaHelperWrapper.asHelper(), 0, Attributes.EMPTY);
|
||||
setOrcaReportConfig(orcaHelperWrapper, SHORT_INTERVAL_CONFIG);
|
||||
deliverSubchannelState(0, ConnectivityStateInfo.forNonError(READY));
|
||||
verify(mockStateListeners[0]).onSubchannelState(eq(ConnectivityStateInfo.forNonError(READY)));
|
||||
|
||||
assertThat(orcaServiceImps[0].calls).hasSize(1);
|
||||
assertLog(subchannels[0].logs,
|
||||
"DEBUG: Starting ORCA reporting for " + subchannels[0].getAllAddresses());
|
||||
assertThat(orcaServiceImps[0].calls.poll().request)
|
||||
.isEqualTo(buildOrcaRequestFromConfig(SHORT_INTERVAL_CONFIG));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateReportingIntervalWhenRpcActive() {
|
||||
// Sets report interval before creating a Subchannel, reporting starts right after suchannel
|
||||
// state becomes READY.
|
||||
setOrcaReportConfig(orcaHelperWrapper, MEDIUM_INTERVAL_CONFIG);
|
||||
createSubchannel(orcaHelperWrapper.asHelper(), 0, Attributes.EMPTY);
|
||||
deliverSubchannelState(0, ConnectivityStateInfo.forNonError(READY));
|
||||
verify(mockStateListeners[0]).onSubchannelState(eq(ConnectivityStateInfo.forNonError(READY)));
|
||||
|
||||
assertThat(orcaServiceImps[0].calls).hasSize(1);
|
||||
assertLog(subchannels[0].logs,
|
||||
"DEBUG: Starting ORCA reporting for " + subchannels[0].getAllAddresses());
|
||||
assertThat(orcaServiceImps[0].calls.peek().request)
|
||||
.isEqualTo(buildOrcaRequestFromConfig(MEDIUM_INTERVAL_CONFIG));
|
||||
|
||||
// Make reporting less frequent.
|
||||
setOrcaReportConfig(orcaHelperWrapper, LONG_INTERVAL_CONFIG);
|
||||
assertThat(orcaServiceImps[0].calls.poll().cancelled).isTrue();
|
||||
assertThat(orcaServiceImps[0].calls).hasSize(1);
|
||||
assertLog(subchannels[0].logs,
|
||||
"DEBUG: Starting ORCA reporting for " + subchannels[0].getAllAddresses());
|
||||
assertThat(orcaServiceImps[0].calls.peek().request)
|
||||
.isEqualTo(buildOrcaRequestFromConfig(LONG_INTERVAL_CONFIG));
|
||||
|
||||
// Configuring with the same report interval again does not restart ORCA RPC.
|
||||
setOrcaReportConfig(orcaHelperWrapper, LONG_INTERVAL_CONFIG);
|
||||
assertThat(orcaServiceImps[0].calls.peek().cancelled).isFalse();
|
||||
assertThat(subchannels[0].logs).isEmpty();
|
||||
|
||||
// Make reporting more frequent.
|
||||
setOrcaReportConfig(orcaHelperWrapper, SHORT_INTERVAL_CONFIG);
|
||||
assertThat(orcaServiceImps[0].calls.poll().cancelled).isTrue();
|
||||
assertThat(orcaServiceImps[0].calls).hasSize(1);
|
||||
assertLog(subchannels[0].logs,
|
||||
"DEBUG: Starting ORCA reporting for " + subchannels[0].getAllAddresses());
|
||||
assertThat(orcaServiceImps[0].calls.poll().request)
|
||||
.isEqualTo(buildOrcaRequestFromConfig(SHORT_INTERVAL_CONFIG));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateReportingIntervalWhenRpcPendingRetry() {
|
||||
createSubchannel(orcaHelperWrapper.asHelper(), 0, Attributes.EMPTY);
|
||||
setOrcaReportConfig(orcaHelperWrapper, SHORT_INTERVAL_CONFIG);
|
||||
deliverSubchannelState(0, ConnectivityStateInfo.forNonError(READY));
|
||||
verify(mockStateListeners[0]).onSubchannelState(eq(ConnectivityStateInfo.forNonError(READY)));
|
||||
|
||||
assertThat(orcaServiceImps[0].calls).hasSize(1);
|
||||
assertLog(subchannels[0].logs,
|
||||
"DEBUG: Starting ORCA reporting for " + subchannels[0].getAllAddresses());
|
||||
assertThat(orcaServiceImps[0].calls.peek().request)
|
||||
.isEqualTo(buildOrcaRequestFromConfig(SHORT_INTERVAL_CONFIG));
|
||||
|
||||
// Server closes the RPC without response, client will retry with backoff.
|
||||
assertThat(fakeClock.getPendingTasks()).isEmpty();
|
||||
orcaServiceImps[0].calls.poll().responseObserver.onCompleted();
|
||||
assertLog(subchannels[0].logs,
|
||||
"DEBUG: ORCA reporting stream closed with " + Status.OK + ", backoff in 11"
|
||||
+ " ns");
|
||||
assertThat(fakeClock.getPendingTasks()).hasSize(1);
|
||||
assertThat(orcaServiceImps[0].calls).isEmpty();
|
||||
|
||||
// Make reporting less frequent.
|
||||
setOrcaReportConfig(orcaHelperWrapper, LONG_INTERVAL_CONFIG);
|
||||
// Retry task will be canceled and restarts new RPC immediately.
|
||||
assertThat(fakeClock.getPendingTasks()).isEmpty();
|
||||
assertThat(orcaServiceImps[0].calls).hasSize(1);
|
||||
assertLog(subchannels[0].logs,
|
||||
"DEBUG: Starting ORCA reporting for " + subchannels[0].getAllAddresses());
|
||||
assertThat(orcaServiceImps[0].calls.peek().request)
|
||||
.isEqualTo(buildOrcaRequestFromConfig(LONG_INTERVAL_CONFIG));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void policiesReceiveSameReportIndependently() {
|
||||
createSubchannel(childHelperWrapper.asHelper(), 0, Attributes.EMPTY);
|
||||
deliverSubchannelState(0, ConnectivityStateInfo.forNonError(READY));
|
||||
|
||||
// No helper sets ORCA reporting interval, so load reporting is not started.
|
||||
verify(mockStateListeners[0]).onSubchannelState(eq(ConnectivityStateInfo.forNonError(READY)));
|
||||
assertThat(orcaServiceImps[0].calls).isEmpty();
|
||||
assertThat(subchannels[0].logs).isEmpty();
|
||||
|
||||
// Parent helper requests ORCA reports with a certain interval, load reporting starts.
|
||||
setOrcaReportConfig(parentHelperWrapper, SHORT_INTERVAL_CONFIG);
|
||||
assertThat(orcaServiceImps[0].calls).hasSize(1);
|
||||
assertLog(subchannels[0].logs,
|
||||
"DEBUG: Starting ORCA reporting for " + subchannels[0].getAllAddresses());
|
||||
|
||||
OrcaLoadReport report = OrcaLoadReport.getDefaultInstance();
|
||||
assertThat(orcaServiceImps[0].calls).hasSize(1);
|
||||
orcaServiceImps[0].calls.peek().responseObserver.onNext(report);
|
||||
assertLog(subchannels[0].logs, "DEBUG: Received an ORCA report: " + report);
|
||||
// Only parent helper's listener receives the report.
|
||||
ArgumentCaptor<OrcaLoadReport> parentReportCaptor = ArgumentCaptor.forClass(null);
|
||||
verify(mockOrcaListener1).onLoadReport(parentReportCaptor.capture());
|
||||
assertThat(parentReportCaptor.getValue()).isEqualTo(report);
|
||||
verifyNoMoreInteractions(mockOrcaListener2);
|
||||
|
||||
// Now child helper also wants to receive reports.
|
||||
setOrcaReportConfig(childHelperWrapper, SHORT_INTERVAL_CONFIG);
|
||||
orcaServiceImps[0].calls.peek().responseObserver.onNext(report);
|
||||
assertLog(subchannels[0].logs, "DEBUG: Received an ORCA report: " + report);
|
||||
// Both helper receives the same report instance.
|
||||
ArgumentCaptor<OrcaLoadReport> childReportCaptor = ArgumentCaptor.forClass(null);
|
||||
verify(mockOrcaListener1, times(2))
|
||||
.onLoadReport(parentReportCaptor.capture());
|
||||
verify(mockOrcaListener2)
|
||||
.onLoadReport(childReportCaptor.capture());
|
||||
assertThat(childReportCaptor.getValue()).isSameInstanceAs(parentReportCaptor.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportWithMostFrequentIntervalRequested() {
|
||||
setOrcaReportConfig(parentHelperWrapper, SHORT_INTERVAL_CONFIG);
|
||||
setOrcaReportConfig(childHelperWrapper, LONG_INTERVAL_CONFIG);
|
||||
createSubchannel(childHelperWrapper.asHelper(), 0, Attributes.EMPTY);
|
||||
deliverSubchannelState(0, ConnectivityStateInfo.forNonError(READY));
|
||||
verify(mockStateListeners[0]).onSubchannelState(eq(ConnectivityStateInfo.forNonError(READY)));
|
||||
assertThat(orcaServiceImps[0].calls).hasSize(1);
|
||||
assertLog(subchannels[0].logs,
|
||||
"DEBUG: Starting ORCA reporting for " + subchannels[0].getAllAddresses());
|
||||
|
||||
// The real report interval to be requested is the minimum of intervals requested by helpers.
|
||||
assertThat(Durations.toNanos(orcaServiceImps[0].calls.peek().request.getReportInterval()))
|
||||
.isEqualTo(SHORT_INTERVAL_CONFIG.getReportIntervalNanos());
|
||||
|
||||
// Child helper wants reporting to be more frequent than its current setting while it is still
|
||||
// less frequent than parent helper. Nothing should happen on existing RPC.
|
||||
setOrcaReportConfig(childHelperWrapper, MEDIUM_INTERVAL_CONFIG);
|
||||
assertThat(orcaServiceImps[0].calls.peek().cancelled).isFalse();
|
||||
assertThat(subchannels[0].logs).isEmpty();
|
||||
|
||||
// Parent helper wants reporting to be less frequent.
|
||||
setOrcaReportConfig(parentHelperWrapper, MEDIUM_INTERVAL_CONFIG);
|
||||
assertThat(orcaServiceImps[0].calls.poll().cancelled).isTrue();
|
||||
assertThat(orcaServiceImps[0].calls).hasSize(1);
|
||||
assertLog(subchannels[0].logs,
|
||||
"DEBUG: Starting ORCA reporting for " + subchannels[0].getAllAddresses());
|
||||
// ORCA reporting RPC restarts and the the real report interval is adjusted.
|
||||
assertThat(Durations.toNanos(orcaServiceImps[0].calls.poll().request.getReportInterval()))
|
||||
.isEqualTo(MEDIUM_INTERVAL_CONFIG.getReportIntervalNanos());
|
||||
}
|
||||
|
||||
private void verifyRetryAfterNanos(InOrder inOrder, OpenRcaServiceImp orcaServiceImp,
|
||||
long nanos) {
|
||||
assertThat(fakeClock.getPendingTasks()).hasSize(1);
|
||||
assertThat(orcaServiceImp.calls).isEmpty();
|
||||
fakeClock.forwardNanos(nanos - 1);
|
||||
assertThat(orcaServiceImp.calls).isEmpty();
|
||||
inOrder.verifyNoMoreInteractions();
|
||||
fakeClock.forwardNanos(1);
|
||||
assertThat(orcaServiceImp.calls).hasSize(1);
|
||||
assertThat(fakeClock.getPendingTasks()).isEmpty();
|
||||
}
|
||||
|
||||
private void deliverSubchannelState(final int index, final ConnectivityStateInfo newState) {
|
||||
syncContext.execute(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
subchannels[index].stateListener.onSubchannelState(newState);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Subchannel createSubchannel(final Helper helper, final int index,
|
||||
final Attributes attrs) {
|
||||
final AtomicReference<Subchannel> newSubchannel = new AtomicReference<>();
|
||||
syncContext.execute(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Subchannel s =
|
||||
helper.createSubchannel(
|
||||
CreateSubchannelArgs.newBuilder()
|
||||
.setAddresses(eagLists[index])
|
||||
.setAttributes(attrs)
|
||||
.build());
|
||||
s.start(mockStateListeners[index]);
|
||||
newSubchannel.set(s);
|
||||
}
|
||||
});
|
||||
return newSubchannel.get();
|
||||
}
|
||||
|
||||
private void setOrcaReportConfig(
|
||||
final OrcaReportingHelperWrapper helperWrapper, final OrcaReportingConfig config) {
|
||||
syncContext.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
helperWrapper.setReportingConfig(config);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static final class OpenRcaServiceImp extends OpenRcaServiceGrpc.OpenRcaServiceImplBase {
|
||||
final Queue<ServerSideCall> calls = new ArrayDeque<>();
|
||||
|
||||
@Override
|
||||
public void streamCoreMetrics(
|
||||
OrcaLoadReportRequest request, StreamObserver<OrcaLoadReport> responseObserver) {
|
||||
final ServerSideCall call = new ServerSideCall(request, responseObserver);
|
||||
Context.current()
|
||||
.addListener(
|
||||
new CancellationListener() {
|
||||
@Override
|
||||
public void cancelled(Context ctx) {
|
||||
call.cancelled = true;
|
||||
}
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
calls.add(call);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ServerSideCall {
|
||||
final OrcaLoadReportRequest request;
|
||||
final StreamObserver<OrcaLoadReport> responseObserver;
|
||||
boolean cancelled;
|
||||
|
||||
ServerSideCall(OrcaLoadReportRequest request, StreamObserver<OrcaLoadReport> responseObserver) {
|
||||
this.request = request;
|
||||
this.responseObserver = responseObserver;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FakeSocketAddress extends SocketAddress {
|
||||
final String name;
|
||||
|
||||
FakeSocketAddress(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
private final class FakeSubchannel extends Subchannel {
|
||||
final List<EquivalentAddressGroup> eagList;
|
||||
final Attributes attrs;
|
||||
final Channel channel;
|
||||
final List<String> logs = new ArrayList<>();
|
||||
final int index;
|
||||
SubchannelStateListener stateListener;
|
||||
private final ChannelLogger logger =
|
||||
new ChannelLogger() {
|
||||
@Override
|
||||
public void log(ChannelLogLevel level, String msg) {
|
||||
logs.add(level + ": " + msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(ChannelLogLevel level, String template, Object... args) {
|
||||
log(level, MessageFormat.format(template, args));
|
||||
}
|
||||
};
|
||||
|
||||
FakeSubchannel(int index, CreateSubchannelArgs args, Channel channel) {
|
||||
this.index = index;
|
||||
this.eagList = args.getAddresses();
|
||||
this.attrs = args.getAttributes();
|
||||
this.channel = checkNotNull(channel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(SubchannelStateListener listener) {
|
||||
checkState(this.stateListener == null);
|
||||
this.stateListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
deliverSubchannelState(index, ConnectivityStateInfo.forNonError(SHUTDOWN));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestConnection() {
|
||||
throw new AssertionError("Should not be called");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EquivalentAddressGroup> getAllAddresses() {
|
||||
return eagList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Attributes getAttributes() {
|
||||
return attrs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Channel asChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelLogger getChannelLogger() {
|
||||
return logger;
|
||||
}
|
||||
}
|
||||
|
||||
private final class FakeHelper extends Helper {
|
||||
@Override
|
||||
public Subchannel createSubchannel(CreateSubchannelArgs args) {
|
||||
int index = -1;
|
||||
for (int i = 0; i < NUM_SUBCHANNELS; i++) {
|
||||
if (eagLists[i].equals(args.getAddresses())) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
checkState(index >= 0, "addrs " + args.getAddresses() + " not found");
|
||||
FakeSubchannel subchannel = new FakeSubchannel(index, args, channels[index]);
|
||||
checkState(subchannels[index] == null, "subchannels[" + index + "] already created");
|
||||
subchannels[index] = subchannel;
|
||||
return subchannel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBalancingState(ConnectivityState newState, SubchannelPicker newPicker) {
|
||||
throw new AssertionError("Should not be called");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SynchronizationContext getSynchronizationContext() {
|
||||
return syncContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScheduledExecutorService getScheduledExecutorService() {
|
||||
return fakeClock.getScheduledExecutorService();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public NameResolver.Factory getNameResolverFactory() {
|
||||
throw new AssertionError("Should not be called");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthority() {
|
||||
throw new AssertionError("Should not be called");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ManagedChannel createOobChannel(EquivalentAddressGroup eag, String authority) {
|
||||
throw new AssertionError("Should not be called");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* Copyright 2019 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.xds;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.mockito.AdditionalAnswers.delegatesTo;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.envoyproxy.udpa.data.orca.v1.OrcaLoadReport;
|
||||
import io.grpc.ClientStreamTracer;
|
||||
import io.grpc.Metadata;
|
||||
import io.grpc.xds.OrcaPerRequestUtil.OrcaPerRequestReportListener;
|
||||
import io.grpc.xds.OrcaPerRequestUtil.OrcaReportingTracerFactory;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link OrcaPerRequestUtil} class.
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
public class OrcaPerRequestUtilTest {
|
||||
|
||||
private static final ClientStreamTracer.StreamInfo STREAM_INFO =
|
||||
ClientStreamTracer.StreamInfo.newBuilder().build();
|
||||
|
||||
@Mock
|
||||
private OrcaPerRequestReportListener orcaListener1;
|
||||
@Mock
|
||||
private OrcaPerRequestReportListener orcaListener2;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a single load balance policy's listener receive per-request ORCA reports upon call
|
||||
* trailer arrives.
|
||||
*/
|
||||
@Test
|
||||
public void singlePolicyTypicalWorkflow() {
|
||||
// Use a mocked noop stream tracer factory as the original stream tracer factory.
|
||||
ClientStreamTracer.Factory fakeDelegateFactory = mock(ClientStreamTracer.Factory.class);
|
||||
ClientStreamTracer fakeTracer = mock(ClientStreamTracer.class);
|
||||
doNothing().when(fakeTracer).inboundTrailers(any(Metadata.class));
|
||||
when(fakeDelegateFactory.newClientStreamTracer(
|
||||
any(ClientStreamTracer.StreamInfo.class), any(Metadata.class)))
|
||||
.thenReturn(fakeTracer);
|
||||
|
||||
// The OrcaReportingTracerFactory will augment the StreamInfo passed to its
|
||||
// newClientStreamTracer method. The augmented StreamInfo's CallOptions will contain
|
||||
// a OrcaReportBroker, in which has the registered listener.
|
||||
ClientStreamTracer.Factory factory =
|
||||
OrcaPerRequestUtil.getInstance()
|
||||
.newOrcaClientStreamTracerFactory(fakeDelegateFactory, orcaListener1);
|
||||
ClientStreamTracer tracer = factory.newClientStreamTracer(STREAM_INFO, new Metadata());
|
||||
ArgumentCaptor<ClientStreamTracer.StreamInfo> streamInfoCaptor = ArgumentCaptor.forClass(null);
|
||||
verify(fakeDelegateFactory)
|
||||
.newClientStreamTracer(streamInfoCaptor.capture(), any(Metadata.class));
|
||||
ClientStreamTracer.StreamInfo capturedInfo = streamInfoCaptor.getValue();
|
||||
assertThat(capturedInfo).isNotEqualTo(STREAM_INFO);
|
||||
|
||||
// When the trailer does not contain ORCA report, listener callback will not be invoked.
|
||||
Metadata trailer = new Metadata();
|
||||
tracer.inboundTrailers(trailer);
|
||||
verifyNoMoreInteractions(orcaListener1);
|
||||
|
||||
// When the trailer contains an ORCA report, listener callback will be invoked.
|
||||
trailer.put(
|
||||
OrcaReportingTracerFactory.ORCA_ENDPOINT_LOAD_METRICS_KEY,
|
||||
OrcaLoadReport.getDefaultInstance());
|
||||
tracer.inboundTrailers(trailer);
|
||||
ArgumentCaptor<OrcaLoadReport> reportCaptor = ArgumentCaptor.forClass(null);
|
||||
verify(orcaListener1).onLoadReport(reportCaptor.capture());
|
||||
assertThat(reportCaptor.getValue()).isEqualTo(OrcaLoadReport.getDefaultInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests parent-child load balance policies' listeners both receive per-request ORCA reports upon
|
||||
* call trailer arrives and ORCA report deserialization happens only once.
|
||||
*/
|
||||
@Test
|
||||
public void twoLevelPoliciesTypicalWorkflow() {
|
||||
ClientStreamTracer.Factory parentFactory =
|
||||
mock(ClientStreamTracer.Factory.class,
|
||||
delegatesTo(
|
||||
OrcaPerRequestUtil.getInstance().newOrcaClientStreamTracerFactory(orcaListener1)));
|
||||
|
||||
ClientStreamTracer.Factory childFactory =
|
||||
OrcaPerRequestUtil.getInstance()
|
||||
.newOrcaClientStreamTracerFactory(parentFactory, orcaListener2);
|
||||
// Child factory will augment the StreamInfo and pass it to the parent factory.
|
||||
ClientStreamTracer childTracer =
|
||||
childFactory.newClientStreamTracer(STREAM_INFO, new Metadata());
|
||||
ArgumentCaptor<ClientStreamTracer.StreamInfo> streamInfoCaptor = ArgumentCaptor.forClass(null);
|
||||
verify(parentFactory).newClientStreamTracer(streamInfoCaptor.capture(), any(Metadata.class));
|
||||
ClientStreamTracer.StreamInfo parentStreamInfo = streamInfoCaptor.getValue();
|
||||
assertThat(parentStreamInfo).isNotEqualTo(STREAM_INFO);
|
||||
|
||||
// When the trailer does not contain ORCA report, no listener callback will be invoked.
|
||||
Metadata trailer = new Metadata();
|
||||
childTracer.inboundTrailers(trailer);
|
||||
verifyNoMoreInteractions(orcaListener1);
|
||||
verifyNoMoreInteractions(orcaListener2);
|
||||
|
||||
// When the trailer contains an ORCA report, callbacks for both listeners will be invoked.
|
||||
// Both listener will receive the same ORCA report instance, which means deserialization
|
||||
// happens only once.
|
||||
trailer.put(
|
||||
OrcaReportingTracerFactory.ORCA_ENDPOINT_LOAD_METRICS_KEY,
|
||||
OrcaLoadReport.getDefaultInstance());
|
||||
childTracer.inboundTrailers(trailer);
|
||||
ArgumentCaptor<OrcaLoadReport> parentReportCap = ArgumentCaptor.forClass(null);
|
||||
ArgumentCaptor<OrcaLoadReport> childReportCap = ArgumentCaptor.forClass(null);
|
||||
verify(orcaListener1).onLoadReport(parentReportCap.capture());
|
||||
verify(orcaListener2).onLoadReport(childReportCap.capture());
|
||||
assertThat(parentReportCap.getValue()).isEqualTo(OrcaLoadReport.getDefaultInstance());
|
||||
assertThat(childReportCap.getValue()).isSameInstanceAs(parentReportCap.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the case when parent policy creates its own {@link ClientStreamTracer.Factory}, ORCA
|
||||
* reports are only forwarded to the parent's listener.
|
||||
*/
|
||||
@Test
|
||||
public void onlyParentPolicyReceivesReportsIfCreatesOwnTracer() {
|
||||
ClientStreamTracer.Factory parentFactory =
|
||||
OrcaPerRequestUtil.getInstance().newOrcaClientStreamTracerFactory(orcaListener1);
|
||||
ClientStreamTracer.Factory childFactory =
|
||||
mock(ClientStreamTracer.Factory.class,
|
||||
delegatesTo(OrcaPerRequestUtil.getInstance()
|
||||
.newOrcaClientStreamTracerFactory(parentFactory, orcaListener2)));
|
||||
ClientStreamTracer parentTracer =
|
||||
parentFactory.newClientStreamTracer(STREAM_INFO, new Metadata());
|
||||
Metadata trailer = new Metadata();
|
||||
trailer.put(
|
||||
OrcaReportingTracerFactory.ORCA_ENDPOINT_LOAD_METRICS_KEY,
|
||||
OrcaLoadReport.getDefaultInstance());
|
||||
parentTracer.inboundTrailers(trailer);
|
||||
verify(orcaListener1).onLoadReport(eq(OrcaLoadReport.getDefaultInstance()));
|
||||
verifyZeroInteractions(childFactory);
|
||||
verifyZeroInteractions(orcaListener2);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue