package com.bazaarvoice.ostrich.discovery.zookeeper; import com.bazaarvoice.curator.recipes.NodeDiscovery; import com.bazaarvoice.ostrich.HostDiscovery; import com.bazaarvoice.ostrich.ServiceEndPoint; import com.bazaarvoice.ostrich.ServiceEndPointJsonCodec; import com.bazaarvoice.ostrich.metrics.Metrics; import com.codahale.metrics.Counter; import com.codahale.metrics.Gauge; import com.codahale.metrics.Meter; import com.codahale.metrics.MetricRegistry; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Charsets; import com.google.common.collect.ConcurrentHashMultiset; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Multiset; import com.google.common.collect.Sets; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.utils.ZKPaths; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Set; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; /** * The <code>HostDiscovery</code> class encapsulates a ZooKeeper backed NodeDiscovery which watches a specific service * path in ZooKeeper and will monitor which end points are known to exist. As end pionts come and go the results of * calling the {@link #getHosts} method change. */ public class ZooKeeperHostDiscovery implements HostDiscovery { private static final Logger LOG = LoggerFactory.getLogger(ZooKeeperHostDiscovery.class); /** * The root path in ZooKeeper for where service registrations are stored. * <p/> * WARNING: Do not modify this without also modifying the ALL of the corresponding paths in the service registry, * host discovery, and service discovery classes!!! */ @VisibleForTesting static final String ROOT_SERVICES_PATH = "/ostrich"; private final NodeDiscovery<ServiceEndPoint> _nodeDiscovery; private final Multiset<ServiceEndPoint> _endPoints; private final Set<EndPointListener> _listeners; private final Metrics.InstanceMetrics _metrics; private final Counter _numListeners; private final Meter _numZooKeeperAdds; private final Meter _numZooKeeperRemoves; private final Meter _numZooKeeperChanges; public ZooKeeperHostDiscovery(CuratorFramework curator, String serviceName, MetricRegistry metrics) { this(new NodeDiscoveryFactory(), curator, serviceName, metrics); } @VisibleForTesting ZooKeeperHostDiscovery(NodeDiscoveryFactory factory, CuratorFramework curator, String serviceName, MetricRegistry metrics) { checkNotNull(factory); checkNotNull(curator); checkNotNull(serviceName); checkArgument(!"".equals(serviceName)); checkNotNull(metrics); String servicePath = makeServicePath(serviceName); _listeners = Sets.newSetFromMap(Maps.<EndPointListener, Boolean>newConcurrentMap()); _endPoints = ConcurrentHashMultiset.create(); _nodeDiscovery = factory.create( curator, servicePath, new NodeDiscovery.NodeDataParser<ServiceEndPoint>() { public ServiceEndPoint parse(String path, byte[] nodeData) { String json = new String(nodeData, Charsets.UTF_8); return ServiceEndPointJsonCodec.fromJson(json); } } ); _nodeDiscovery.addListener(new ServiceListener()); _metrics = Metrics.forInstance(metrics, this, serviceName); _metrics.gauge("num-end-points", new Gauge<Integer>() { @Override public Integer getValue() { return Iterables.size(getHosts()); } }); _numListeners = _metrics.counter("num-listeners"); _numZooKeeperAdds = _metrics.meter("num-zookeeper-adds"); _numZooKeeperRemoves = _metrics.meter("num-zookeeper-removes"); _numZooKeeperChanges = _metrics.meter("num-zookeeper-changes"); // wait to start node discovery until all fields are initialized. _nodeDiscovery.start(); } @Override public Iterable<ServiceEndPoint> getHosts() { return Iterables.unmodifiableIterable(_endPoints.elementSet()); } @Override public void addListener(EndPointListener listener) { _listeners.add(listener); _numListeners.inc(); } @Override public void removeListener(EndPointListener listener) { _listeners.remove(listener); _numListeners.dec(); } @Override public void close() throws IOException { _nodeDiscovery.close(); _endPoints.clear(); _metrics.close(); } @VisibleForTesting void addServiceEndPoint(ServiceEndPoint serviceEndPoint) { // add returns the number of instances that were in the Multiset before the add. if (_endPoints.add(serviceEndPoint, 1) == 0) { fireAddEvent(serviceEndPoint); } } @VisibleForTesting void removeServiceEndPoint(ServiceEndPoint serviceEndPoint) { // remove returns the number of instances that were in the Multiset before the remove. if (_endPoints.remove(serviceEndPoint, 1) == 1) { fireRemoveEvent(serviceEndPoint); } } private void fireAddEvent(ServiceEndPoint endPoint) { for (EndPointListener listener : _listeners) { listener.onEndPointAdded(endPoint); } } private void fireRemoveEvent(ServiceEndPoint endPoint) { for (EndPointListener listener : _listeners) { listener.onEndPointRemoved(endPoint); } } /** * Construct the path in ZooKeeper to where a service's children live. * @param serviceName The name of the service to get the ZooKeeper path for. * @return The ZooKeeper path. */ public static String makeServicePath(String serviceName) { checkNotNull(serviceName); checkArgument(!"".equals(serviceName)); return ZKPaths.makePath(ROOT_SERVICES_PATH, serviceName); } /** * A zookeeper-common {@code NodeListener} */ private final class ServiceListener implements NodeDiscovery.NodeListener<ServiceEndPoint> { @Override public void onNodeAdded(String path, ServiceEndPoint node) { _numZooKeeperAdds.mark(); addServiceEndPoint(node); } @Override public void onNodeRemoved(String path, ServiceEndPoint node) { _numZooKeeperRemoves.mark(); removeServiceEndPoint(node); } @Override public void onNodeUpdated(String path, ServiceEndPoint node) { _numZooKeeperChanges.mark(); LOG.info("ServiceEndPoint data changed unexpectedly. End point ID: {}; ZooKeeperPath {}", node.getId(), path); } } @VisibleForTesting static class NodeDiscoveryFactory { NodeDiscovery<ServiceEndPoint> create(CuratorFramework curator, String path, NodeDiscovery.NodeDataParser<ServiceEndPoint> parser) { return new NodeDiscovery<>(curator, path, parser); } } }