// ================================================================================================= // 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.zookeeper; import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.Logger; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.testing.TearDown; import org.apache.thrift.protocol.TProtocol; import org.apache.zookeeper.ZooDefs; import org.apache.zookeeper.data.ACL; import org.junit.Before; import org.junit.Test; import com.twitter.common.net.pool.DynamicHostSet; import com.twitter.common.thrift.TResourceExhaustedException; import com.twitter.common.thrift.Thrift; import com.twitter.common.thrift.ThriftFactory; import com.twitter.common.thrift.ThriftFactory.ThriftFactoryException; import com.twitter.common.zookeeper.Group.JoinException; import com.twitter.common.zookeeper.ServerSet.EndpointStatus; import com.twitter.common.zookeeper.testing.BaseZooKeeperTest; import com.twitter.thrift.Endpoint; import com.twitter.thrift.ServiceInstance; import com.twitter.thrift.Status; import static org.junit.Assert.*; /** * * TODO(William Farner): Change this to remove thrift dependency. * * @author John Sirois */ public class ServerSetImplTest extends BaseZooKeeperTest { private static final Logger LOG = Logger.getLogger(ServerSetImpl.class.getName()); private static final List<ACL> ACL = ZooDefs.Ids.OPEN_ACL_UNSAFE; private static final String SERVICE = "/twitter/services/puffin_hosebird"; private LinkedBlockingQueue<ImmutableSet<ServiceInstance>> serverSetBuffer; private DynamicHostSet.HostChangeMonitor<ServiceInstance> serverSetMonitor; @Before public void mySetUp() throws IOException { serverSetBuffer = new LinkedBlockingQueue<ImmutableSet<ServiceInstance>>(); serverSetMonitor = new DynamicHostSet.HostChangeMonitor<ServiceInstance>() { @Override public void onChange(ImmutableSet<ServiceInstance> serverSet) { serverSetBuffer.offer(serverSet); } }; } private ServerSetImpl createServerSet() throws IOException { return new ServerSetImpl(createZkClient(), ACL, SERVICE); } @Test public void testLifecycle() throws Exception { ServerSetImpl client = createServerSet(); client.monitor(serverSetMonitor); assertChangeFiredEmpty(); ServerSetImpl server = createServerSet(); EndpointStatus status = server.join(InetSocketAddress.createUnresolved("foo", 1234), makePortMap("http-admin", 8080), Status.ALIVE); ServiceInstance serviceInstance = new ServiceInstance(new Endpoint("foo", 1234), ImmutableMap.of("http-admin", new Endpoint("foo", 8080)), Status.ALIVE); assertChangeFired(serviceInstance); status.update(Status.STOPPING); assertChangeFired(serviceInstance.deepCopy().setStatus(Status.STOPPING)); expireSession(server.getZkClient()); assertChangeFiredEmpty(); // We should've auto re-joined in our previous state. assertChangeFired(serviceInstance.deepCopy().setStatus(Status.STOPPING)); // Membership does not change during our monitor's expiration so we should not be notified. expireSession(client.getZkClient()); status.update(Status.STOPPED); assertChangeFired(serviceInstance.deepCopy().setStatus(Status.STOPPED)); // Neither membership nor status changed, so we should not be notified. status.update(Status.STOPPED); status.update(Status.DEAD); assertChangeFiredEmpty(); assertTrue(serverSetBuffer.isEmpty()); } @Test public void testMembershipChanges() throws Exception { ServerSetImpl client = createServerSet(); client.monitor(serverSetMonitor); assertChangeFiredEmpty(); ServerSetImpl server = createServerSet(); EndpointStatus foo = join(server, "foo"); assertChangeFired("foo"); expireSession(client.getZkClient()); EndpointStatus bar = join(server, "bar"); // We should've auto re-monitored membership, but not been notifed of "foo" since this was not a // change, just "foo", "bar" since this was an addition. assertChangeFired("foo", "bar"); foo.update(Status.DEAD); assertChangeFired("bar"); EndpointStatus baz = join(server, "baz"); assertChangeFired("bar", "baz"); baz.update(Status.DEAD); assertChangeFired("bar"); bar.update(Status.DEAD); assertChangeFiredEmpty(); assertTrue(serverSetBuffer.isEmpty()); } @Test public void testOrdering() throws Exception { ServerSetImpl client = createServerSet(); client.monitor(serverSetMonitor); assertChangeFiredEmpty(); Map<String, InetSocketAddress> server1Ports = makePortMap("http-admin1", 8080); Map<String, InetSocketAddress> server2Ports = makePortMap("http-admin2", 8081); Map<String, InetSocketAddress> server3Ports = makePortMap("http-admin3", 8082); ServerSetImpl server1 = createServerSet(); ServerSetImpl server2 = createServerSet(); ServerSetImpl server3 = createServerSet(); ServiceInstance instance1 = new ServiceInstance(new Endpoint("foo", 1000), ImmutableMap.of("http-admin1", new Endpoint("foo", 8080)), Status.ALIVE); ServiceInstance instance2 = new ServiceInstance(new Endpoint("foo", 1001), ImmutableMap.of("http-admin2", new Endpoint("foo", 8081)), Status.ALIVE); ServiceInstance instance3 = new ServiceInstance(new Endpoint("foo", 1002), ImmutableMap.of("http-admin3", new Endpoint("foo", 8082)), Status.ALIVE); EndpointStatus status1 = server1.join(InetSocketAddress.createUnresolved("foo", 1000), server1Ports, Status.ALIVE); assertEquals(ImmutableList.of(instance1), ImmutableList.copyOf(serverSetBuffer.take())); EndpointStatus status2 = server2.join(InetSocketAddress.createUnresolved("foo", 1001), server2Ports, Status.ALIVE); assertEquals(ImmutableList.of(instance1, instance2), ImmutableList.copyOf(serverSetBuffer.take())); EndpointStatus status3 = server3.join(InetSocketAddress.createUnresolved("foo", 1002), server3Ports, Status.ALIVE); assertEquals(ImmutableList.of(instance1, instance2, instance3), ImmutableList.copyOf(serverSetBuffer.take())); status2.update(Status.DEAD); assertEquals(ImmutableList.of(instance1, instance3), ImmutableList.copyOf(serverSetBuffer.take())); } //TODO(Jake Mannix) move this test method to ServerSetConnectionPoolTest, which should be renamed to //DynamicBackendConnectionPoolTest, and refactor assertChangeFired* methods to be used both //here and there @Test public void testThriftWithServerSet() throws Exception { final AtomicReference<Socket> clientConnection = new AtomicReference<Socket>(); final CountDownLatch connected = new CountDownLatch(1); final ServerSocket server = new ServerSocket(0); Thread service = new Thread(new Runnable() { @Override public void run() { try { clientConnection.set(server.accept()); } catch (IOException e) { LOG.log(Level.WARNING, "Problem accepting a connection to thrift server", e); } finally { connected.countDown(); } } }); service.setDaemon(true); service.start(); ServerSetImpl serverSetImpl = new ServerSetImpl(createZkClient(), SERVICE); serverSetImpl.monitor(serverSetMonitor); assertChangeFiredEmpty(); InetSocketAddress localSocket = new InetSocketAddress(server.getLocalPort()); EndpointStatus status = serverSetImpl.join(localSocket, Maps.<String, InetSocketAddress>newHashMap(), Status.STARTING); assertChangeFired(ImmutableMap.<InetSocketAddress, Status>of(localSocket, Status.STARTING)); Service.Iface svc = createThriftClient(serverSetImpl); try { svc.getString(); fail("ServerSet is currently empty, should throw exception here."); } catch (TResourceExhaustedException e) { assertTrue(true); } status.update(Status.ALIVE); assertChangeFired(ImmutableMap.<InetSocketAddress, Status>of(localSocket, Status.ALIVE)); svc = createThriftClient(serverSetImpl); try { String value = svc.getString(); LOG.info("Got value: " + value + " from server"); assertEquals(Service.Iface.DONE, value); } catch (TResourceExhaustedException e) { fail("ServerSet is not empty, should not throw exception here"); } finally { connected.await(); server.close(); } } private Service.Iface createThriftClient(DynamicHostSet<ServiceInstance> serverSet) throws ThriftFactoryException { final Thrift<Service.Iface> thrift = ThriftFactory.create(Service.Iface.class).build(serverSet); addTearDown(new TearDown() { @Override public void tearDown() { thrift.close(); } }); return thrift.create(); } private static Map<String, InetSocketAddress> makePortMap(String name, int port) { return ImmutableMap.of(name, InetSocketAddress.createUnresolved("foo", port)); } public static class Service { public static interface Iface { public static final String DONE = "done"; public String getString() throws TResourceExhaustedException; } public static class Client implements Iface { public Client(TProtocol protocol) { assertNotNull(protocol); } @Override public String getString() { return DONE; } } } private EndpointStatus join(ServerSet serverSet, String host) throws JoinException, InterruptedException { return serverSet.join(InetSocketAddress.createUnresolved(host, 42), ImmutableMap.<String, InetSocketAddress>of(), Status.ALIVE); } private void assertChangeFired(Map<InetSocketAddress, Status> hostsStatuses) throws InterruptedException { assertChangeFired( ImmutableSet.copyOf(Iterables.transform(ImmutableSet.copyOf(hostsStatuses.entrySet()), new Function<Map.Entry<InetSocketAddress, Status>, ServiceInstance>() { @Override public ServiceInstance apply(Map.Entry<InetSocketAddress, Status> e) { return new ServiceInstance(new Endpoint(e.getKey().getHostName(), e.getKey().getPort()), ImmutableMap.<String, Endpoint>of(), e.getValue()); } }))); } private void assertChangeFired(String... serviceHosts) throws InterruptedException { assertChangeFired(ImmutableSet.copyOf(Iterables.transform(ImmutableSet.copyOf(serviceHosts), new Function<String, ServiceInstance>() { @Override public ServiceInstance apply(String serviceHost) { return new ServiceInstance(new Endpoint(serviceHost, 42), ImmutableMap.<String, Endpoint>of(), Status.ALIVE); } }))); } protected void assertChangeFiredEmpty() throws InterruptedException { assertChangeFired(ImmutableSet.<ServiceInstance>of()); } protected void assertChangeFired(ServiceInstance... serviceInstances) throws InterruptedException { assertChangeFired(ImmutableSet.copyOf(serviceInstances)); } protected void assertChangeFired(ImmutableSet<ServiceInstance> serviceInstances) throws InterruptedException { assertEquals(serviceInstances, serverSetBuffer.take()); } }