com.netflix.loadbalancer.reactive.LoadBalancerCommand Maven / Gradle / Ivy
/*
*
* 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 extends ExecutionListener, T>> 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 extends ExecutionListener, T>> 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, T> 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 super Server> 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