// ================================================================================================= // Copyright 2011 Twitter, Inc. // ------------------------------------------------------------------------------------------------- // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this work except in compliance with the License. // You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.net.loadbalancing; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.twitter.common.base.Closure; import com.twitter.common.net.pool.ResourceExhaustedException; import com.twitter.common.net.loadbalancing.RequestTracker.RequestResult; import com.twitter.common.util.BackoffDecider; import java.util.Collection; import java.util.Map; import java.util.Set; import java.util.logging.Logger; /** * A load balancer that serves as a layer above another load balancer to mark hosts as dead, and * prevent them from being visible to the wrapped load balancer. * If all backends become marked as dead, they will all be unmarked. * * @author William Farner */ public class MarkDeadStrategy<S> implements LoadBalancingStrategy<S> { private static final Logger LOG = Logger.getLogger(MarkDeadStrategy.class.getName()); private final LoadBalancingStrategy<S> wrappedStrategy; private final Map<S, BackoffDecider> targets = Maps.newHashMap(); private final Function<S, BackoffDecider> backoffFactory; protected final Predicate<S> hostChecker; private Set<S> liveBackends = null; private Closure<Collection<S>> onBackendsChosen = null; // Flipped when we are in "forced live" mode, where all backends are considered dead and we // send them all traffic as a last-ditch effort. private boolean forcedLive = false; /** * Creates a mark dead strategy with a wrapped strategy, backoff decider factory * and a predicate host checker. Use this constructor if you want to pass in the * your own implementation of the host checker. * * @param wrappedStrategy one of the implementations of the load balancing strategy. * @param backoffFactory backoff decider factory per host. * @param hostChecker predicate that returns {@code true} if the host is alive, otherwise returns {@code false}. */ public MarkDeadStrategy(LoadBalancingStrategy<S> wrappedStrategy, Function<S, BackoffDecider> backoffFactory, Predicate<S> hostChecker) { this.wrappedStrategy = Preconditions.checkNotNull(wrappedStrategy); this.backoffFactory = Preconditions.checkNotNull(backoffFactory); this.hostChecker = Preconditions.checkNotNull(hostChecker); } /** * Constructor that uses a default predicate host checker that always returns true. * This is the default constructor that all consumers of MarkDeadStrategy currently use. * * @param wrappedStrategy one of the implementations of the load balancing strategy. * @param backoffFactory backoff decider factory per host. */ public MarkDeadStrategy(LoadBalancingStrategy<S> wrappedStrategy, Function<S, BackoffDecider> backoffFactory) { this(wrappedStrategy, backoffFactory, Predicates.<S>alwaysTrue()); } @Override public void offerBackends(Set<S> offeredBackends, Closure<Collection<S>> onBackendsChosen) { this.onBackendsChosen = onBackendsChosen; targets.keySet().retainAll(offeredBackends); for (S backend : offeredBackends) { if (!targets.containsKey(backend)) { targets.put(backend, backoffFactory.apply(backend)); } } adjustBackends(); } @Override public void addConnectResult(S backendKey, ConnectionResult result, long connectTimeNanos) { Preconditions.checkNotNull(backendKey); Preconditions.checkNotNull(result); BackoffDecider decider = targets.get(backendKey); Preconditions.checkNotNull(decider); addResult(decider, result); if (shouldNotifyFor(backendKey)) { wrappedStrategy.addConnectResult(backendKey, result, connectTimeNanos); } } @Override public void connectionReturned(S backendKey) { Preconditions.checkNotNull(backendKey); if (shouldNotifyFor(backendKey)) { wrappedStrategy.connectionReturned(backendKey); } } @Override public void addRequestResult(S requestKey, RequestResult result, long requestTimeNanos) { Preconditions.checkNotNull(requestKey); Preconditions.checkNotNull(result); BackoffDecider decider = targets.get(requestKey); Preconditions.checkNotNull(decider); addResult(decider, result); if (shouldNotifyFor(requestKey)) { wrappedStrategy.addRequestResult(requestKey, result, requestTimeNanos); } } private void addResult(BackoffDecider decider, ConnectionResult result) { switch (result) { case FAILED: case TIMEOUT: addResult(decider, false); break; case SUCCESS: addResult(decider, true); break; default: throw new UnsupportedOperationException("Unhandled result type " + result); } } private void addResult(BackoffDecider decider, RequestTracker.RequestResult result) { switch (result) { case FAILED: case TIMEOUT: addResult(decider, false); break; case SUCCESS: addResult(decider, true); break; default: throw new UnsupportedOperationException("Unhandled result type " + result); } } private void addResult(BackoffDecider decider, boolean success) { if (success) { decider.addSuccess(); } else { decider.addFailure(); } // Check if any of the backends have moved into or out of dead state. for (Map.Entry<S, BackoffDecider> entry : targets.entrySet()) { boolean dead = entry.getValue().shouldBackOff(); boolean markedDead = !liveBackends.contains(entry.getKey()); // only check the servers that were marked dead before and see if we can // connect to them, otherwise set dead to true. if (markedDead && !dead) { boolean alive = hostChecker.apply(entry.getKey()); if (!alive) { entry.getValue().transitionToBackOff(0, true); } dead = !alive; } if (dead && !markedDead && forcedLive) { // Do nothing here. Since we have forced all backends to be live, we don't want to // continually advertise the backend list to the wrapped strategy. } else if (dead != markedDead || !dead && forcedLive) { adjustBackends(); break; } } } private boolean shouldNotifyFor(S backend) { return liveBackends.contains(backend); } private final Predicate<S> deadTargetFilter = new Predicate<S>() { @Override public boolean apply(S backend) { return !targets.get(backend).shouldBackOff(); } }; private void adjustBackends() { liveBackends = Sets.newHashSet(Iterables.filter(targets.keySet(), deadTargetFilter)); if (liveBackends.isEmpty()) { liveBackends = targets.keySet(); forcedLive = true; } else { forcedLive = false; } LOG.info("Observed backend state change, changing live backends to " + liveBackends); wrappedStrategy.offerBackends(liveBackends, onBackendsChosen); } @Override public S nextBackend() throws ResourceExhaustedException { return wrappedStrategy.nextBackend(); } }