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

io.grpc.xds.orca.OrcaOobUtil Maven / Gradle / Ivy

The newest version!
/*
 * 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.orca;

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 com.github.xds.data.orca.v3.OrcaLoadReport;
import com.github.xds.service.orca.v3.OpenRcaServiceGrpc;
import com.github.xds.service.orca.v3.OrcaLoadReportRequest;
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.grpc.Attributes;
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.services.MetricReport;
import io.grpc.util.ForwardingLoadBalancerHelper;
import io.grpc.util.ForwardingSubchannel;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;

/**
 * Utility class that provides method for {@link LoadBalancer} to install listeners to receive
 * out-of-band backend metrics in the format of Open Request Cost Aggregation (ORCA).
 */
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/9129")
public final class OrcaOobUtil {

  private OrcaOobUtil() {}

  /**
   * Creates a new {@link io.grpc.LoadBalancer.Helper} with provided
   * {@link OrcaOobReportListener} installed
   * to receive callback when an out-of-band ORCA report is received.
   *
   * 

Example usages: * *

    *
  • Leaf policy (e.g., WRR policy) *
       *       {@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.
       *           OrcaReportingHelper orcaHelper =
       *               OrcaOobUtil.newOrcaReportingHelper(originHelper);
       *           Subchannel subchannel =
       *               orcaHelper.createSubchannel(CreateSubchannelArgs.newBuilder()...);
       *           OrcaOobUtil.setListener(
       *              subchannel,
       *              listener,
       *              OrcaRerportingConfig.newBuilder().setReportInterval(30, SECOND).build());
       *           ...
       *         }
       *       }
       *       }
       *     
    *
  • *
  • Delegating policy doing per-child-policy aggregation *
       *       {@code
       *       class XdsLoadBalancer extends LoadBalancer {
       *         private final Helper orcaHelper;  // the original Helper
       *
       *         public XdsLoadBalancer(LoadBalancer.Helper helper) {
       *           this.orcaHelper = OrcaUtil.newOrcaReportingHelper(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.
       *           LoadBalancer childLb = childPolicyProvider.newLoadBalancer(
       *             new ForwardingLoadBalancerHelper() {
       *               public Subchannel createSubchannel(CreateSubchannelArgs args) {
       *                 Subchannel subchannel = super.createSubchannel(args);
       *                 OrcaOobUtil.setListener(subchannel, listener,
       *                 OrcaReportingConfig.newBuilder().setReportInterval(30, SECOND).build());
       *                 return subchannel;
       *               }
       *               public LoadBalancer.Helper delegate() {
       *                 return orcaHelper;
       *               }
       *             });
       *         }
       *       }
       *       }
       *     
    *
  • *
* * @param delegate the delegate helper that provides essentials for establishing subchannels to * backends. */ public static LoadBalancer.Helper newOrcaReportingHelper(LoadBalancer.Helper delegate) { return newOrcaReportingHelper( delegate, new ExponentialBackoffPolicy.Provider(), GrpcUtil.STOPWATCH_SUPPLIER); } @VisibleForTesting static LoadBalancer.Helper newOrcaReportingHelper( LoadBalancer.Helper delegate, BackoffPolicy.Provider backoffPolicyProvider, Supplier stopwatchSupplier) { return new OrcaReportingHelper(delegate, backoffPolicyProvider, stopwatchSupplier); } /** * 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. * *

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 grpc {@link MetricReport}. */ void onLoadReport(MetricReport report); } static final Attributes.Key ORCA_REPORTING_STATE_KEY = Attributes.Key.create("internal-orca-reporting-state"); /** * Update {@link OrcaOobReportListener} to receive Out-of-Band metrics report for the * particular subchannel connection, and set the configuration of receiving ORCA reports, * such as the interval of receiving reports. Set listener to null to remove listener, and the * config will have no effect. * *

This method needs to be called from the SynchronizationContext returned by the wrapped * helper's {@link Helper#getSynchronizationContext()}. * *

Each load balancing policy must call this method to configure the backend load reporting. * Otherwise, it will not receive ORCA reports. * *

If multiple load balancing policies configure reporting with different intervals, reports * come with the minimum of those intervals. * * @param subchannel the server connected by this subchannel to receive the metrics. * * @param listener the callback upon receiving backend metrics from the Out-Of-Band stream. * Setting to null to removes the listener from the subchannel. * * @param config the configuration to be set. It has no effect when listener is null. * */ public static void setListener(Subchannel subchannel, OrcaOobReportListener listener, OrcaReportingConfig config) { Attributes attributes = subchannel.getAttributes(); SubchannelImpl orcaSubchannel = (attributes == null) ? null : attributes.get(ORCA_REPORTING_STATE_KEY); if (orcaSubchannel == null) { throw new IllegalArgumentException("Subchannel does not have orca Out-Of-Band stream enabled." + " Try to use a subchannel created by OrcaOobUtil.OrcaHelper."); } orcaSubchannel.orcaState.setListener(orcaSubchannel, listener, config); } /** * 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. Subchannels created through it will retrieve ORCA load reports if the server * supports it. */ static final class OrcaReportingHelper extends ForwardingLoadBalancerHelper { private final LoadBalancer.Helper delegate; private final SynchronizationContext syncContext; private final BackoffPolicy.Provider backoffPolicyProvider; private final Supplier stopwatchSupplier; OrcaReportingHelper( LoadBalancer.Helper delegate, BackoffPolicy.Provider backoffPolicyProvider, Supplier stopwatchSupplier) { this.delegate = checkNotNull(delegate, "delegate"); 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(); Subchannel subchannel = super.createSubchannel(args); Attributes attributes = subchannel.getAttributes(); SubchannelImpl orcaSubchannel = (attributes == null) ? null : attributes.get(ORCA_REPORTING_STATE_KEY); OrcaReportingState orcaState; if (orcaSubchannel == null) { // Only the first load balancing policy requesting ORCA reports instantiates an // OrcaReportingState. orcaState = new OrcaReportingState(syncContext, delegate().getScheduledExecutorService()); } else { orcaState = orcaSubchannel.orcaState; } return new SubchannelImpl(subchannel, orcaState); } /** * An {@link OrcaReportingState} is a client of ORCA service running on a single backend. * *

All methods are run from {@code syncContext}. */ private final class OrcaReportingState implements SubchannelStateListener { private final SynchronizationContext syncContext; private final ScheduledExecutorService timeService; private final Map 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; private boolean started; OrcaReportingState( SynchronizationContext syncContext, ScheduledExecutorService timeService) { 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"); started = true; } void setListener(SubchannelImpl orcaSubchannel, OrcaOobReportListener listener, OrcaReportingConfig config) { syncContext.execute(new Runnable() { @Override public void run() { OrcaOobReportListener oldListener = orcaSubchannel.reportListener; if (oldListener != null) { configs.remove(oldListener); } if (listener != null) { configs.put(listener, config); } orcaSubchannel.reportListener = listener; setReportingConfig(config); } }); } private void setReportingConfig(OrcaReportingConfig config) { boolean reconfigured = false; // Real reporting interval is the minimum of intervals requested by all participating // helpers. if (configs.isEmpty()) { overallConfig = null; reconfigured = true; } else 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; } 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 { private final ClientCall 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); MetricReport metricReport = OrcaPerRequestUtil.fromOrcaLoadReport(response); for (OrcaOobReportListener listener : configs.keySet()) { listener.onLoadReport(metricReport); } call.request(1); } void handleStreamClosed(Status status) { if (Objects.equal(status.getCode(), Code.UNIMPLEMENTED)) { disabled = true; subchannelLogger.log( ChannelLogLevel.ERROR, "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; @Nullable private OrcaOobReportListener reportListener; 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) { if (!orcaState.started) { orcaState.init(this, listener); super.start(orcaState); } else { super.start(listener); } } @Override public Attributes getAttributes() { return super.getAttributes().toBuilder().set(ORCA_REPORTING_STATE_KEY, this).build(); } } /** 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); } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy