package com.twitter.common.zookeeper;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Map;
import com.google.common.base.Joiner;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.twitter.common.base.Command;
import com.twitter.common.base.Commands;
import com.twitter.common.base.MorePreconditions;
import com.twitter.common.zookeeper.Group.JoinException;
import com.twitter.thrift.ServiceInstance;
import com.twitter.thrift.Status;
/**
* A ServerSet that delegates all calls to other ServerSets.
*/
public class CompoundServerSet implements ServerSet {
private static final Joiner STACK_TRACE_JOINER = Joiner.on('\n');
private final List<ServerSet> serverSets;
private final Map<ServerSet, ImmutableSet<ServiceInstance>> instanceCache = Maps.newHashMap();
private final List<HostChangeMonitor<ServiceInstance>> monitors = Lists.newArrayList();
private Command stopWatching = null;
private ImmutableSet<ServiceInstance> allHosts = ImmutableSet.of();
/**
* Create new ServerSet from a list of serverSets.
*
* @param serverSets serverSets to which the calls will be delegated.
*/
public CompoundServerSet(Iterable<ServerSet> serverSets) {
MorePreconditions.checkNotBlank(serverSets);
this.serverSets = ImmutableList.copyOf(serverSets);
}
private interface JoinOp {
EndpointStatus doJoin(ServerSet serverSet) throws JoinException, InterruptedException;
}
private interface StatusOp {
void changeStatus(EndpointStatus status) throws UpdateException;
}
private void changeStatus(
ImmutableList<EndpointStatus> statuses,
StatusOp statusOp) throws UpdateException {
ImmutableList.Builder<String> builder = ImmutableList.builder();
int errorIdx = 1;
for (EndpointStatus endpointStatus : statuses) {
try {
statusOp.changeStatus(endpointStatus);
} catch (UpdateException exception) {
builder.add(String.format("[%d] %s", errorIdx++,
Throwables.getStackTraceAsString(exception)));
}
}
if (errorIdx > 1) {
throw new UpdateException(
"One or more ServerSet update failed: " + STACK_TRACE_JOINER.join(builder.build()));
}
}
private EndpointStatus doJoin(JoinOp joiner) throws JoinException, InterruptedException {
// Get the list of endpoint status from the serverSets.
ImmutableList.Builder<EndpointStatus> builder = ImmutableList.builder();
for (ServerSet serverSet : serverSets) {
builder.add(joiner.doJoin(serverSet));
}
final ImmutableList<EndpointStatus> statuses = builder.build();
return new EndpointStatus() {
@Override public void leave() throws UpdateException {
changeStatus(statuses, new StatusOp() {
@Override public void changeStatus(EndpointStatus status) throws UpdateException {
status.leave();
}
});
}
@Override public void update(final Status newStatus) throws UpdateException {
changeStatus(statuses, new StatusOp() {
@Override public void changeStatus(EndpointStatus status) throws UpdateException {
status.update(newStatus);
}
});
}
};
}
@Override
public EndpointStatus join(
final InetSocketAddress endpoint,
final Map<String, InetSocketAddress> additionalEndpoints)
throws Group.JoinException, InterruptedException {
return doJoin(new JoinOp() {
@Override public EndpointStatus doJoin(ServerSet serverSet)
throws JoinException, InterruptedException {
return serverSet.join(endpoint, additionalEndpoints);
}
});
}
/*
* If any one of the serverSet throws an exception during respective join, the exception is
* propagated. Join is successful only if all the joins are successful.
*
* NOTE: If an exception occurs during the join, the serverSets in the composite can be in a
* partially joined state.
*
* @see ServerSet#join(InetSocketAddress, Map, Status)
*/
@Override
public EndpointStatus join(
final InetSocketAddress endpoint,
final Map<String, InetSocketAddress> additionalEndpoints,
final Status status) throws Group.JoinException, InterruptedException {
return doJoin(new JoinOp() {
@Override public EndpointStatus doJoin(ServerSet serverSet)
throws JoinException, InterruptedException {
return serverSet.join(endpoint, additionalEndpoints, status);
}
});
}
@Override
public EndpointStatus join(
final InetSocketAddress endpoint,
final Map<String, InetSocketAddress> additionalEndpoints,
final int shardId) throws JoinException, InterruptedException {
return doJoin(new JoinOp() {
@Override public EndpointStatus doJoin(ServerSet serverSet)
throws JoinException, InterruptedException {
return serverSet.join(endpoint, additionalEndpoints, shardId);
}
});
}
// Handles changes to the union of hosts.
private synchronized void handleChange(ServerSet serverSet, ImmutableSet<ServiceInstance> hosts) {
instanceCache.put(serverSet, hosts);
// Get the union of hosts.
ImmutableSet<ServiceInstance> currentHosts =
ImmutableSet.copyOf(Iterables.concat(instanceCache.values()));
// Check if the hosts have changed.
if (!currentHosts.equals(allHosts)) {
allHosts = currentHosts;
// Notify the monitors.
for (HostChangeMonitor<ServiceInstance> monitor : monitors) {
monitor.onChange(allHosts);
}
}
}
/**
* Monitor the CompoundServerSet.
*
* If any one of the monitor calls to the underlying serverSet raises a MonitorException, the
* exception is propagated. The call is successful only if all the monitor calls to the
* underlying serverSets are successful.
*
* NOTE: If an exception occurs during the monitor call, the serverSets in the composite will not
* be monitored.
*
* @param monitor HostChangeMonitor instance used to monitor host changes.
* @return A command that, when executed, will stop monitoring all underlying server sets.
* @throws MonitorException If there was a problem monitoring any of the underlying server sets.
*/
@Override
public synchronized Command watch(HostChangeMonitor<ServiceInstance> monitor)
throws MonitorException {
if (stopWatching == null) {
monitors.add(monitor);
ImmutableList.Builder<Command> commandsBuilder = ImmutableList.builder();
for (final ServerSet serverSet : serverSets) {
commandsBuilder.add(serverSet.watch(new HostChangeMonitor<ServiceInstance>() {
@Override public void onChange(ImmutableSet<ServiceInstance> hostSet) {
handleChange(serverSet, hostSet);
}
}));
}
stopWatching = Commands.compound(commandsBuilder.build());
}
return stopWatching;
}
@Override
public void monitor(HostChangeMonitor<ServiceInstance> monitor) throws MonitorException {
watch(monitor);
}
}