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

com.netflix.loadbalancer.reactive.LoadBalancerCommand Maven / Gradle / Ivy

There is a newer version: 2.7.18
Show newest version
/*
 *
 * Copyright 2014 Netflix, Inc.
 *
 * 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 com.netflix.loadbalancer.reactive;

import java.net.URI;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import rx.Observable;
import rx.Observable.OnSubscribe;
import rx.Observer;
import rx.Subscriber;
import rx.functions.Func1;
import rx.functions.Func2;

import com.netflix.client.ClientException;
import com.netflix.client.RetryHandler;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.LoadBalancerContext;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerStats;
import com.netflix.loadbalancer.reactive.ExecutionListener.AbortExecutionException;
import com.netflix.servo.monitor.Stopwatch;

/**
 * A command that is used to produce the Observable from the load balancer execution. The load balancer is responsible for
 * the following:
 *
 * 
    *
  • Choose a server
  • *
  • Invoke the {@link #call(com.netflix.loadbalancer.Server)} method
  • *
  • Invoke the {@link ExecutionListener} if any
  • *
  • Retry on exception, controlled by {@link com.netflix.client.RetryHandler}
  • *
  • Provide feedback to the {@link com.netflix.loadbalancer.LoadBalancerStats}
  • *
* * @author Allen Wang */ public class LoadBalancerCommand { private static final Logger logger = LoggerFactory.getLogger(LoadBalancerCommand.class); public static class Builder { private RetryHandler retryHandler; private ILoadBalancer loadBalancer; private IClientConfig config; private LoadBalancerContext loadBalancerContext; private List> listeners; private Object loadBalancerKey; private ExecutionContext executionContext; private ExecutionContextListenerInvoker invoker; private URI loadBalancerURI; private Server server; private Builder() {} public Builder withLoadBalancer(ILoadBalancer loadBalancer) { this.loadBalancer = loadBalancer; return this; } public Builder withLoadBalancerURI(URI loadBalancerURI) { this.loadBalancerURI = loadBalancerURI; return this; } public Builder withListeners(List> listeners) { if (this.listeners == null) { this.listeners = new LinkedList>(listeners); } else { this.listeners.addAll((Collection) listeners); } return this; } public Builder withRetryHandler(RetryHandler retryHandler) { this.retryHandler = retryHandler; return this; } public Builder withClientConfig(IClientConfig config) { this.config = config; return this; } /** * Pass in an optional key object to help the load balancer to choose a specific server among its * server list, depending on the load balancer implementation. */ public Builder withServerLocator(Object key) { this.loadBalancerKey = key; return this; } public Builder withLoadBalancerContext(LoadBalancerContext loadBalancerContext) { this.loadBalancerContext = loadBalancerContext; return this; } public Builder withExecutionContext(ExecutionContext executionContext) { this.executionContext = executionContext; return this; } /** * Pin the operation to a specific server. Otherwise run on any server returned by the load balancer * * @param server */ public Builder withServer(Server server) { this.server = server; return this; } public LoadBalancerCommand build() { if (loadBalancerContext == null && loadBalancer == null) { throw new IllegalArgumentException("Either LoadBalancer or LoadBalancerContext needs to be set"); } if (listeners != null && listeners.size() > 0) { this.invoker = new ExecutionContextListenerInvoker(executionContext, listeners, config); } if (loadBalancerContext == null) { loadBalancerContext = new LoadBalancerContext(loadBalancer, config); } return new LoadBalancerCommand(this); } } public static Builder builder() { return new Builder(); } private final URI loadBalancerURI; private final Object loadBalancerKey; private final LoadBalancerContext loadBalancerContext; private final RetryHandler retryHandler; private volatile ExecutionInfo executionInfo; private final Server server; private final ExecutionContextListenerInvoker listenerInvoker; private LoadBalancerCommand(Builder builder) { this.loadBalancerURI = builder.loadBalancerURI; this.loadBalancerKey = builder.loadBalancerKey; this.loadBalancerContext = builder.loadBalancerContext; this.retryHandler = builder.retryHandler != null ? builder.retryHandler : loadBalancerContext.getRetryHandler(); this.listenerInvoker = builder.invoker; this.server = builder.server; } /** * Return an Observable that either emits only the single requested server * or queries the load balancer for the next server on each subscription */ private Observable selectServer() { return Observable.create(new OnSubscribe() { @Override public void call(Subscriber next) { try { Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey); next.onNext(server); next.onCompleted(); } catch (Exception e) { next.onError(e); } } }); } class ExecutionInfoContext { Server server; int serverAttemptCount = 0; int attemptCount = 0; public void setServer(Server server) { this.server = server; this.serverAttemptCount++; this.attemptCount = 0; } public void incAttemptCount() { this.attemptCount++; } public int getAttemptCount() { return attemptCount; } public Server getServer() { return server; } public int getServerAttemptCount() { return this.serverAttemptCount; } public ExecutionInfo toExecutionInfo() { return ExecutionInfo.create(server, attemptCount-1, serverAttemptCount-1); } public ExecutionInfo toFinalExecutionInfo() { return ExecutionInfo.create(server, attemptCount, serverAttemptCount-1); } } private Func2 retryPolicy(final int maxRetrys, final boolean same) { return new Func2() { @Override public Boolean call(Integer tryCount, Throwable e) { if (e instanceof AbortExecutionException) { return false; } if (tryCount > maxRetrys) { return false; } if (e.getCause() != null && e instanceof RuntimeException) { e = e.getCause(); } return retryHandler.isRetriableException(e, same); } }; } /** * Create an {@link Observable} that once subscribed execute network call asynchronously with a server chosen by load balancer. * If there are any errors that are indicated as retriable by the {@link RetryHandler}, they will be consumed internally by the * function and will not be observed by the {@link Observer} subscribed to the returned {@link Observable}. If number of retries has * exceeds the maximal allowed, a final error will be emitted by the returned {@link Observable}. Otherwise, the first successful * result during execution and retries will be emitted. */ public Observable submit(final ServerOperation operation) { final ExecutionInfoContext context = new ExecutionInfoContext(); if (listenerInvoker != null) { try { listenerInvoker.onExecutionStart(); } catch (AbortExecutionException e) { return Observable.error(e); } } final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer(); final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer(); // Use the load balancer Observable o = (server == null ? selectServer() : Observable.just(server)) .concatMap(new Func1>() { @Override // Called for each server being selected public Observable call(Server server) { context.setServer(server); final ServerStats stats = loadBalancerContext.getServerStats(server); // Called for each attempt and retry Observable o = Observable .just(server) .concatMap(new Func1>() { @Override public Observable call(final Server server) { context.incAttemptCount(); loadBalancerContext.noteOpenConnection(stats); if (listenerInvoker != null) { try { listenerInvoker.onStartWithServer(context.toExecutionInfo()); } catch (AbortExecutionException e) { return Observable.error(e); } } final Stopwatch tracer = loadBalancerContext.getExecuteTracer().start(); return operation.call(server).doOnEach(new Observer() { private T entity; @Override public void onCompleted() { recordStats(tracer, stats, entity, null); // TODO: What to do if onNext or onError are never called? } @Override public void onError(Throwable e) { recordStats(tracer, stats, null, e); logger.debug("Got error {} when executed on server {}", e, server); if (listenerInvoker != null) { listenerInvoker.onExceptionWithServer(e, context.toExecutionInfo()); } } @Override public void onNext(T entity) { this.entity = entity; if (listenerInvoker != null) { listenerInvoker.onExecutionSuccess(entity, context.toExecutionInfo()); } } private void recordStats(Stopwatch tracer, ServerStats stats, Object entity, Throwable exception) { tracer.stop(); loadBalancerContext.noteRequestCompletion(stats, entity, exception, tracer.getDuration(TimeUnit.MILLISECONDS), retryHandler); } }); } }); if (maxRetrysSame > 0) o = o.retry(retryPolicy(maxRetrysSame, true)); return o; } }); if (maxRetrysNext > 0 && server == null) o = o.retry(retryPolicy(maxRetrysNext, false)); return o.onErrorResumeNext(new Func1>() { @Override public Observable call(Throwable e) { if (context.getAttemptCount() > 0) { if (maxRetrysNext > 0 && context.getServerAttemptCount() == (maxRetrysNext + 1)) { e = new ClientException(ClientException.ErrorType.NUMBEROF_RETRIES_NEXTSERVER_EXCEEDED, "Number of retries on next server exceeded max " + maxRetrysNext + " retries, while making a call for: " + context.getServer(), e); } else if (maxRetrysSame > 0 && context.getAttemptCount() == (maxRetrysSame + 1)) { e = new ClientException(ClientException.ErrorType.NUMBEROF_RETRIES_EXEEDED, "Number of retries exceeded max " + maxRetrysSame + " retries, while making a call for: " + context.getServer(), e); } } if (listenerInvoker != null) { listenerInvoker.onExecutionFailed(e, context.toFinalExecutionInfo()); } return Observable.error(e); } }); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy