// =================================================================================================
// 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 java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.logging.Logger;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.twitter.common.net.pool.ResourceExhaustedException;
/**
* A load balancer that attempts to direct load towards a backend that has the fewest leased
* connections.
*
* @author William Farner
*/
public class LeastConnectedStrategy<S> extends StaticLoadBalancingStrategy<S> {
private static final Logger LOG = Logger.getLogger(LeastConnectedStrategy.class.getName());
// Maps from backends to the number of connections made to them.
private final Map<S, ConnectionStats> connections = Maps.newHashMap();
// Manages sorting of connection counts, with a reference back to the backend.
private final SortedSet<ConnectionStats> connectionStats = Sets.newTreeSet();
/**
* Encapsulates a set of connection stats that allow connections to be sorted as per the least
* connected strategy.
*/
private class ConnectionStats implements Comparable<ConnectionStats> {
final S connectionKey;
final int connectionId;
int activeCount = 0; // Stores the total number of active connections.
long useCount = 0; // Stores the total number times a connection has been used.
ConnectionStats(S connectionKey, int connectionId) {
this.connectionKey = connectionKey;
this.connectionId = connectionId;
}
@Override
public int compareTo(ConnectionStats other) {
// Sort by number of active connections first.
int difference = activeCount - other.activeCount;
if (difference != 0) {
return difference;
}
// Sub-sort by total number of times a connection has been used (this will ensure that
// all backends are exercised).
long useDifference = useCount - other.useCount;
if (useDifference != 0) {
return Long.signum(useDifference);
}
// If the above two are equal, break the tie using the connection id.
return connectionId - other.connectionId;
}
@Override
public boolean equals(Object o) {
// We use ConnectionStats in a sorted container and so we need to have an equals
// implementation consistent with compareTo, ie:
// (x.compareTo(y) == 0) == x.equals(y)
// We accomplish this directly.
@SuppressWarnings("unchecked")
ConnectionStats other = (ConnectionStats) o;
return compareTo(other) == 0;
}
@Override
public String toString() {
return String.format("%d-%d", activeCount, useCount);
}
}
@Override
protected Collection<S> onBackendsOffered(Set<S> backends) {
Map<S, ConnectionStats> newConnections = Maps.newHashMapWithExpectedSize(backends.size());
Collection<ConnectionStats> newConnectionStats =
Lists.newArrayListWithCapacity(backends.size());
// Recreate all connection stats since their ordering may have changed and this is used for
// comparison tie breaks.
int backendId = 0;
for (S backend : backends) {
ConnectionStats stats = new ConnectionStats(backend, backendId++);
// Retain the activeCount for existing backends to prevent dogpiling existing active servers
ConnectionStats existing = connections.get(backend);
if (existing != null) {
stats.activeCount = existing.activeCount;
}
newConnections.put(backend, stats);
newConnectionStats.add(stats);
}
connections.clear();
connections.putAll(newConnections);
connectionStats.clear();
connectionStats.addAll(newConnectionStats);
return connections.keySet();
}
@Override
public S nextBackend() throws ResourceExhaustedException {
Preconditions.checkState(connections.size() == connectionStats.size());
if (connectionStats.isEmpty()) {
throw new ResourceExhaustedException("No backends.");
}
return connectionStats.first().connectionKey;
}
@Override
public void addConnectResult(S backendKey, ConnectionResult result, long connectTimeNanos) {
Preconditions.checkNotNull(backendKey);
Preconditions.checkState(connections.size() == connectionStats.size());
Preconditions.checkNotNull(result);
ConnectionStats stats = connections.get(backendKey);
Preconditions.checkNotNull(stats);
Preconditions.checkState(connectionStats.remove(stats));
if (result == ConnectionResult.SUCCESS) {
stats.activeCount++;
}
stats.useCount++;
Preconditions.checkState(connectionStats.add(stats));
}
@Override
public void connectionReturned(S backendKey) {
Preconditions.checkNotNull(backendKey);
Preconditions.checkState(connections.size() == connectionStats.size());
ConnectionStats stats = connections.get(backendKey);
Preconditions.checkNotNull(stats);
if (stats.activeCount > 0) {
Preconditions.checkState(connectionStats.remove(stats));
stats.activeCount--;
Preconditions.checkState(connectionStats.add(stats));
} else {
LOG.warning("connection stats dropped below zero, ignoring");
}
}
}