package com.bazaarvoice.ostrich.discovery.zookeeper; import com.bazaarvoice.curator.recipes.NodeDiscovery; import com.bazaarvoice.ostrich.ServiceEndPoint; import com.bazaarvoice.ostrich.ServiceEndPointBuilder; import com.bazaarvoice.ostrich.ServiceEndPointJsonCodec; import com.codahale.metrics.MetricRegistry; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.io.Closeables; import org.apache.curator.framework.CuratorFramework; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Matchers; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.io.IOException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.same; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class ZooKeeperHostDiscoveryTest { private static final ServiceEndPoint FOO = new ServiceEndPointBuilder() .withServiceName("Foo") .withId("server:8080") .build(); private ZooKeeperHostDiscovery _discovery; private NodeDiscovery.NodeListener<ServiceEndPoint> _listener; private NodeDiscovery.NodeDataParser<ServiceEndPoint> _parser; private MetricRegistry _registry = new MetricRegistry(); @SuppressWarnings("unchecked") @Before public void setup() throws Exception { ZooKeeperHostDiscovery.NodeDiscoveryFactory factory = mock(ZooKeeperHostDiscovery.NodeDiscoveryFactory.class); NodeDiscovery<ServiceEndPoint> nodeDiscovery = mock(NodeDiscovery.class); CuratorFramework curator = mock(CuratorFramework.class); when(factory.create(Matchers.any(CuratorFramework.class), anyString(), Matchers.<NodeDiscovery.NodeDataParser<ServiceEndPoint>>any())).thenReturn(nodeDiscovery); _discovery = new ZooKeeperHostDiscovery(factory, curator, FOO.getServiceName(), _registry); // Capture the parser. ArgumentCaptor<NodeDiscovery.NodeDataParser<ServiceEndPoint>> parserCaptor = (ArgumentCaptor) ArgumentCaptor.forClass(NodeDiscovery.NodeDataParser.class); verify(factory).create(same(curator), anyString(), parserCaptor.capture()); _parser = parserCaptor.getValue(); // Capture the listener. ArgumentCaptor<NodeDiscovery.NodeListener<ServiceEndPoint>> listenerCaptor = (ArgumentCaptor) ArgumentCaptor.forClass(NodeDiscovery.NodeListener.class); verify(nodeDiscovery).addListener(listenerCaptor.capture()); _listener = listenerCaptor.getValue(); } @After public void teardown() throws Exception { Closeables.close(_discovery, true); } @Test (expected = NullPointerException.class) public void testNullCurator() { new ZooKeeperHostDiscovery(null, FOO.getServiceName(), mock(MetricRegistry.class)); } @Test (expected = NullPointerException.class) public void testNullServiceName() throws Exception { new ZooKeeperHostDiscovery(mock(CuratorFramework.class), null, mock(MetricRegistry.class)); } @Test (expected = IllegalArgumentException.class) public void testEmptyServiceName() throws Exception { new ZooKeeperHostDiscovery(mock(CuratorFramework.class), "", mock(MetricRegistry.class)); } @Test public void testParser() { assertEquals(FOO, _parser.parse("path", ServiceEndPointJsonCodec.toJson(FOO).getBytes(Charsets.UTF_8))); } @Test public void testStartsEmpty() { assertTrue(Iterables.isEmpty(_discovery.getHosts())); } @Test public void testRegisterService() { addNode("path", FOO); assertEquals(ImmutableList.of(FOO), ImmutableList.copyOf(_discovery.getHosts())); } @Test public void testUnregisterService() { addNode("path", FOO); removeNode("path", FOO); assertTrue(Iterables.isEmpty(_discovery.getHosts())); } @Test public void testClose() throws IOException { // After closing, HostDiscovery returns no hosts so clients won't work if they accidentally keep using it. addNode("path", FOO); _discovery.close(); assertTrue(Iterables.isEmpty(_discovery.getHosts())); _discovery = null; } @SuppressWarnings("unchecked") @Test public void testExistingData() throws Exception { ZooKeeperHostDiscovery.NodeDiscoveryFactory factory = mock(ZooKeeperHostDiscovery.NodeDiscoveryFactory.class); NodeDiscovery<ServiceEndPoint> nodeDiscovery = mock(NodeDiscovery.class); when(factory.create(Matchers.<CuratorFramework>any(), anyString(), Matchers.<NodeDiscovery.NodeDataParser<ServiceEndPoint>>any())).thenReturn(nodeDiscovery); // Capture the listener. final ArgumentCaptor<NodeDiscovery.NodeListener<ServiceEndPoint>> listenerCaptor = (ArgumentCaptor) ArgumentCaptor.forClass(NodeDiscovery.NodeListener.class); doNothing().when(nodeDiscovery).addListener(listenerCaptor.capture()); // Add FOO when nodeDiscovery.start() is called. This is NodeDiscovery's behavior when it has data when started. doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { listenerCaptor.getValue().onNodeAdded("path", FOO); return null; } }).when(nodeDiscovery).start(); ZooKeeperHostDiscovery discovery = new ZooKeeperHostDiscovery(factory, mock(CuratorFramework.class), FOO.getServiceName(), _registry); assertEquals(ImmutableList.of(FOO), ImmutableList.copyOf(discovery.getHosts())); discovery.close(); } @Test public void testDuplicateEntry() { CountingListener endPointListener = new CountingListener(); _discovery.addListener(endPointListener); addNode("path-one", FOO); addNode("path-two", FOO); assertEquals(1, endPointListener.getNumAdds()); assertEquals(ImmutableList.of(FOO), ImmutableList.copyOf(_discovery.getHosts())); } @Test public void testDuplicateEntrySingleRemoval() { CountingListener endPointListener = new CountingListener(); _discovery.addListener(endPointListener); addNode("path-one", FOO); addNode("path-two", FOO); removeNode("path-one", FOO); assertEquals(0, endPointListener.getNumRemoves()); assertEquals(ImmutableList.of(FOO), ImmutableList.copyOf(_discovery.getHosts())); } @Test public void testDuplicateEntryRemoval() { CountingListener endPointListener = new CountingListener(); _discovery.addListener(endPointListener); addNode("path-one", FOO); addNode("path-two", FOO); removeNode("path-one", FOO); removeNode("path-two", FOO); assertEquals(1, endPointListener.getNumRemoves()); assertTrue(Iterables.isEmpty(_discovery.getHosts())); } @Test public void testAlreadyExistingEndPointsDoNotFireEvents() throws Exception { addNode("path", FOO); CountingListener endPointListener = new CountingListener(); _discovery.addListener(endPointListener); assertEquals(0, endPointListener.getNumEvents()); } @Test public void testRegisterServiceCallsListener() throws Exception { CountingListener endPointListener = new CountingListener(); _discovery.addListener(endPointListener); addNode("path", FOO); assertEquals(1, endPointListener.getNumAdds()); } @Test public void testUnregisterServiceCallsListener() throws Exception { CountingListener endPointListener = new CountingListener(); _discovery.addListener(endPointListener); addNode("path", FOO); removeNode("path", FOO); assertEquals(1, endPointListener.getNumRemoves()); } @Test public void testRemovedListenerDoesNotSeeEvents() throws Exception { CountingListener eventCounter = new CountingListener(); _discovery.addListener(eventCounter); _discovery.removeListener(eventCounter); addNode("path", FOO); removeNode("path", FOO); assertEquals(0, eventCounter.getNumEvents()); } @Test public void testMultipleListeners() throws Exception { CountingListener endPointListener1 = new CountingListener(); CountingListener endPointListener2 = new CountingListener(); _discovery.addListener(endPointListener1); _discovery.addListener(endPointListener2); addNode("path", FOO); assertEquals(1, endPointListener1.getNumAdds()); assertEquals(1, endPointListener2.getNumAdds()); removeNode("path", FOO); assertEquals(1, endPointListener1.getNumRemoves()); assertEquals(1, endPointListener2.getNumRemoves()); } private void addNode(String path, ServiceEndPoint endPoint) { _listener.onNodeAdded(path, endPoint); } private void removeNode(String path, ServiceEndPoint endPoint) { _listener.onNodeRemoved(path, endPoint); } private static final class CountingListener implements ZooKeeperHostDiscovery.EndPointListener { private int _numAdds; private int _numRemoves; @Override public void onEndPointAdded(ServiceEndPoint endPoint) { _numAdds++; } @Override public void onEndPointRemoved(ServiceEndPoint endPoint) { _numRemoves++; } public int getNumAdds() { return _numAdds; } public int getNumRemoves() { return _numRemoves; } public int getNumEvents() { return _numAdds + _numRemoves; } } }