/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.zookeeper.client;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Most simple HostProvider, resolves only on instantiation.
*
*/
public final class StaticHostProvider implements HostProvider {
private static final Logger LOG = LoggerFactory
.getLogger(StaticHostProvider.class);
private List<InetSocketAddress> serverAddresses = new ArrayList<InetSocketAddress>(
5);
private Random sourceOfRandomness;
private int lastIndex = -1;
private int currentIndex = -1;
/**
* The following fields are used to migrate clients during reconfiguration
*/
private boolean reconfigMode = false;
private final List<InetSocketAddress> oldServers = new ArrayList<InetSocketAddress>(
5);
private final List<InetSocketAddress> newServers = new ArrayList<InetSocketAddress>(
5);
private int currentIndexOld = -1;
private int currentIndexNew = -1;
private float pOld, pNew;
/**
* Constructs a SimpleHostSet.
*
* @param serverAddresses
* possibly unresolved ZooKeeper server addresses
* @throws IllegalArgumentException
* if serverAddresses is empty or resolves to an empty list
*/
public StaticHostProvider(Collection<InetSocketAddress> serverAddresses) {
sourceOfRandomness = new Random(System.currentTimeMillis() ^ this.hashCode());
this.serverAddresses = resolveAndShuffle(serverAddresses);
if (this.serverAddresses.isEmpty()) {
throw new IllegalArgumentException(
"A HostProvider may not be empty!");
}
currentIndex = -1;
lastIndex = -1;
}
/**
* Constructs a SimpleHostSet. This constructor is used from StaticHostProviderTest to produce deterministic test results
* by initializing sourceOfRandomness with the same seed
*
* @param serverAddresses
* possibly unresolved ZooKeeper server addresses
* @param randomnessSeed a seed used to initialize sourceOfRandomnes
* @throws IllegalArgumentException
* if serverAddresses is empty or resolves to an empty list
*/
public StaticHostProvider(Collection<InetSocketAddress> serverAddresses,
long randomnessSeed) {
sourceOfRandomness = new Random(randomnessSeed);
this.serverAddresses = resolveAndShuffle(serverAddresses);
if (this.serverAddresses.isEmpty()) {
throw new IllegalArgumentException(
"A HostProvider may not be empty!");
}
currentIndex = -1;
lastIndex = -1;
}
private List<InetSocketAddress> resolveAndShuffle(Collection<InetSocketAddress> serverAddresses) {
List<InetSocketAddress> tmpList = new ArrayList<InetSocketAddress>(serverAddresses.size());
for (InetSocketAddress address : serverAddresses) {
try {
InetAddress ia = address.getAddress();
String addr = (ia != null) ? ia.getHostAddress() : address.getHostString();
InetAddress resolvedAddresses[] = InetAddress.getAllByName(addr);
for (InetAddress resolvedAddress : resolvedAddresses) {
InetAddress taddr = InetAddress.getByAddress(address.getHostString(), resolvedAddress.getAddress());
tmpList.add(new InetSocketAddress(taddr, address.getPort()));
}
} catch (UnknownHostException ex) {
LOG.warn("No IP address found for server: {}", address, ex);
}
}
Collections.shuffle(tmpList, sourceOfRandomness);
return tmpList;
}
/**
* Update the list of servers. This returns true if changing connections is necessary for load-balancing, false
* otherwise. Changing connections is necessary if one of the following holds:
* a) the host to which this client is currently connected is not in serverAddresses.
* Otherwise (if currentHost is in the new list serverAddresses):
* b) the number of servers in the cluster is increasing - in this case the load on currentHost should decrease,
* which means that SOME of the clients connected to it will migrate to the new servers. The decision whether
* this client migrates or not (i.e., whether true or false is returned) is probabilistic so that the expected
* number of clients connected to each server is the same.
*
* If true is returned, the function sets pOld and pNew that correspond to the probability to migrate to ones of the
* new servers in serverAddresses or one of the old servers (migrating to one of the old servers is done only
* if our client's currentHost is not in serverAddresses). See nextHostInReconfigMode for the selection logic.
*
* See {@link https://issues.apache.org/jira/browse/ZOOKEEPER-1355} for the protocol and its evaluation, and
* StaticHostProviderTest for the tests that illustrate how load balancing works with this policy.
* @param serverAddresses new host list
* @param currentHost the host to which this client is currently connected
* @return true if changing connections is necessary for load-balancing, false otherwise
*/
@Override
public synchronized boolean updateServerList(
Collection<InetSocketAddress> serverAddresses,
InetSocketAddress currentHost) {
// Resolve server addresses and shuffle them
List<InetSocketAddress> resolvedList = resolveAndShuffle(serverAddresses);
if (resolvedList.isEmpty()) {
throw new IllegalArgumentException(
"A HostProvider may not be empty!");
}
// Check if client's current server is in the new list of servers
boolean myServerInNewConfig = false;
InetSocketAddress myServer = currentHost;
// choose "current" server according to the client rebalancing algorithm
if (reconfigMode) {
myServer = next(0);
}
// if the client is not currently connected to any server
if (myServer == null) {
// reconfigMode = false (next shouldn't return null).
if (lastIndex >= 0) {
// take the last server to which we were connected
myServer = this.serverAddresses.get(lastIndex);
} else {
// take the first server on the list
myServer = this.serverAddresses.get(0);
}
}
for (InetSocketAddress addr : resolvedList) {
if (addr.getPort() == myServer.getPort()
&& ((addr.getAddress() != null
&& myServer.getAddress() != null && addr
.getAddress().equals(myServer.getAddress())) || addr
.getHostString().equals(myServer.getHostString()))) {
myServerInNewConfig = true;
break;
}
}
reconfigMode = true;
newServers.clear();
oldServers.clear();
// Divide the new servers into oldServers that were in the previous list
// and newServers that were not in the previous list
for (InetSocketAddress resolvedAddress : resolvedList) {
if (this.serverAddresses.contains(resolvedAddress)) {
oldServers.add(resolvedAddress);
} else {
newServers.add(resolvedAddress);
}
}
int numOld = oldServers.size();
int numNew = newServers.size();
// number of servers increased
if (numOld + numNew > this.serverAddresses.size()) {
if (myServerInNewConfig) {
// my server is in new config, but load should be decreased.
// Need to decide if this client
// is moving to one of the new servers
if (sourceOfRandomness.nextFloat() <= (1 - ((float) this.serverAddresses
.size()) / (numOld + numNew))) {
pNew = 1;
pOld = 0;
} else {
// do nothing special - stay with the current server
reconfigMode = false;
}
} else {
// my server is not in new config, and load on old servers must
// be decreased, so connect to
// one of the new servers
pNew = 1;
pOld = 0;
}
} else { // number of servers stayed the same or decreased
if (myServerInNewConfig) {
// my server is in new config, and load should be increased, so
// stay with this server and do nothing special
reconfigMode = false;
} else {
pOld = ((float) (numOld * (this.serverAddresses.size() - (numOld + numNew))))
/ ((numOld + numNew) * (this.serverAddresses.size() - numOld));
pNew = 1 - pOld;
}
}
if (!reconfigMode) {
currentIndex = resolvedList.indexOf(getServerAtCurrentIndex());
} else {
currentIndex = -1;
}
this.serverAddresses = resolvedList;
currentIndexOld = -1;
currentIndexNew = -1;
lastIndex = currentIndex;
return reconfigMode;
}
public synchronized InetSocketAddress getServerAtIndex(int i) {
if (i < 0 || i >= serverAddresses.size()) return null;
return serverAddresses.get(i);
}
public synchronized InetSocketAddress getServerAtCurrentIndex() {
return getServerAtIndex(currentIndex);
}
public synchronized int size() {
return serverAddresses.size();
}
/**
* Get the next server to connect to, when in "reconfigMode", which means that
* you've just updated the server list, and now trying to find some server to connect to.
* Once onConnected() is called, reconfigMode is set to false. Similarly, if we tried to connect
* to all servers in new config and failed, reconfigMode is set to false.
*
* While in reconfigMode, we should connect to a server in newServers with probability pNew and to servers in
* oldServers with probability pOld (which is just 1-pNew). If we tried out all servers in either oldServers
* or newServers we continue to try servers from the other set, regardless of pNew or pOld. If we tried all servers
* we give up and go back to the normal round robin mode
*
* When called, this should be protected by synchronized(this)
*/
private InetSocketAddress nextHostInReconfigMode() {
boolean takeNew = (sourceOfRandomness.nextFloat() <= pNew);
// take one of the new servers if it is possible (there are still such
// servers we didn't try),
// and either the probability tells us to connect to one of the new
// servers or if we already
// tried all the old servers
if (((currentIndexNew + 1) < newServers.size())
&& (takeNew || (currentIndexOld + 1) >= oldServers.size())) {
++currentIndexNew;
return newServers.get(currentIndexNew);
}
// start taking old servers
if ((currentIndexOld + 1) < oldServers.size()) {
++currentIndexOld;
return oldServers.get(currentIndexOld);
}
return null;
}
public InetSocketAddress next(long spinDelay) {
boolean needToSleep = false;
InetSocketAddress addr;
synchronized(this) {
if (reconfigMode) {
addr = nextHostInReconfigMode();
if (addr != null) {
currentIndex = serverAddresses.indexOf(addr);
return addr;
}
//tried all servers and couldn't connect
reconfigMode = false;
needToSleep = (spinDelay > 0);
}
++currentIndex;
if (currentIndex == serverAddresses.size()) {
currentIndex = 0;
}
addr = serverAddresses.get(currentIndex);
needToSleep = needToSleep || (currentIndex == lastIndex && spinDelay > 0);
if (lastIndex == -1) {
// We don't want to sleep on the first ever connect attempt.
lastIndex = 0;
}
}
if (needToSleep) {
try {
Thread.sleep(spinDelay);
} catch (InterruptedException e) {
LOG.warn("Unexpected exception", e);
}
}
return addr;
}
public synchronized void onConnected() {
lastIndex = currentIndex;
reconfigMode = false;
}
}