package org.cloudname.service;
import org.cloudname.backends.memory.MemoryBackend;
import org.cloudname.core.BackendManager;
import org.cloudname.core.CloudnameBackend;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.is;
/**
* Test service registration with memory-based backend.
*/
public class CloudnameServiceTest {
private static final CloudnameBackend memoryBackend = BackendManager.getBackend("memory://");
private final ServiceCoordinate coordinate = ServiceCoordinate.parse("service.tag.region");
/**
* Max time to wait for changes to propagate to clients. In seconds.
*/
private static final int MAX_WAIT_S = 1;
private final Random random = new Random();
private int getRandomPort() {
return Math.max(1, Math.abs(random.nextInt(4096)));
}
private ServiceHandle registerService(final CloudnameService cloudnameService, final String serviceCoordinateString) {
final ServiceCoordinate serviceCoordinate = ServiceCoordinate.parse(serviceCoordinateString);
final Endpoint httpEndpoint = new Endpoint("http", "127.0.0.1", getRandomPort());
final Endpoint webconsoleEndpoint = new Endpoint("webconsole", "127.0.0.2", getRandomPort());
final ServiceData serviceData = new ServiceData(Arrays.asList(httpEndpoint, webconsoleEndpoint));
return cloudnameService.registerService(serviceCoordinate, serviceData);
}
/**
* Create two sets of services, register both and check that notifications are sent to the
* subscribers.
*/
@Test
public void testServiceNotifications() throws InterruptedException {
final String SOME_COORDINATE = "someservice.test.local";
final String ANOTHER_COORDINATE = "anotherservice.test.local";
final CloudnameService mainCloudname = new CloudnameService(memoryBackend);
final int numOtherServices = 10;
final List<ServiceHandle> handles = new ArrayList<>();
for (int i = 0; i < numOtherServices; i++) {
handles.add(registerService(mainCloudname, ANOTHER_COORDINATE));
}
final Executor executor = Executors.newCachedThreadPool();
final int numServices = 5;
final CountDownLatch registrationLatch = new CountDownLatch(numServices);
final CountDownLatch instanceLatch = new CountDownLatch(numServices * numOtherServices);
final CountDownLatch httpEndpointLatch = new CountDownLatch(numServices * numOtherServices);
final CountDownLatch webconsoleEndpointLatch = new CountDownLatch(numServices * numOtherServices);
final CountDownLatch removeLatch = new CountDownLatch(numServices * numOtherServices);
final Semaphore terminateSemaphore = new Semaphore(1);
final CountDownLatch completedLatch = new CountDownLatch(numServices);
final Runnable service = new Runnable() {
@Override
public void run() {
try (final CloudnameService cloudnameService = new CloudnameService(memoryBackend)) {
try (final ServiceHandle handle = registerService(cloudnameService, SOME_COORDINATE)) {
registrationLatch.countDown();
final ServiceCoordinate otherServiceCoordinate = ServiceCoordinate.parse(ANOTHER_COORDINATE);
// Do a service lookup on the other service. This will yield N elements.
cloudnameService.addServiceListener(otherServiceCoordinate, new ServiceListener() {
@Override
public void onServiceCreated(final InstanceCoordinate coordinate, final ServiceData data) {
instanceLatch.countDown();
if (data.getEndpoint("http") != null) {
httpEndpointLatch.countDown();
}
if (data.getEndpoint("webconsole") != null) {
webconsoleEndpointLatch.countDown();
}
}
@Override
public void onServiceDataChanged(final InstanceCoordinate coordinate, final ServiceData data) {
if (data.getEndpoint("http") != null) {
httpEndpointLatch.countDown();
}
if (data.getEndpoint("webconsole") != null) {
webconsoleEndpointLatch.countDown();
}
}
@Override
public void onServiceRemoved(final InstanceCoordinate coordinate) {
removeLatch.countDown();
}
});
// Wait for the go ahead before terminating
try {
terminateSemaphore.acquire();
terminateSemaphore.release();
} catch (final InterruptedException ie) {
throw new RuntimeException(ie);
}
}
// The service handle will close and the instance will be removed at this point.
}
completedLatch.countDown();
}
};
// Grab the semaphore. This wil stop the services from terminating
terminateSemaphore.acquire();
// Start two threads which will register a service and look up a set of another.
for (int i = 0; i < numServices; i++) {
executor.execute(service);
}
// Wait for the registrations and endpoints to propagate
assertTrue("Expected registrations to complete",
registrationLatch.await(MAX_WAIT_S, TimeUnit.SECONDS));
assertTrue("Expected http endpoints to be registered but missing "
+ httpEndpointLatch.getCount(),
httpEndpointLatch.await(MAX_WAIT_S, TimeUnit.SECONDS));
assertTrue("Expected webconsole endpoints to be registered but missing "
+ webconsoleEndpointLatch.getCount(),
webconsoleEndpointLatch.await(MAX_WAIT_S, TimeUnit.SECONDS));
// Registrations are now completed; remove the existing services
for (final ServiceHandle handle : handles) {
handle.close();
}
// This will trigger remove events in the threads.
assertTrue("Expected services to be removed but " + removeLatch.getCount()
+ " still remains", removeLatch.await(MAX_WAIT_S, TimeUnit.SECONDS));
// Let the threads terminate. This will remove the registrations
terminateSemaphore.release();
assertTrue("Expected services to complete but " + completedLatch.getCount()
+ " still remains", completedLatch.await(MAX_WAIT_S, TimeUnit.SECONDS));
// Success! There shouldn't be any more services registered at this point. Check to make sure
mainCloudname.addServiceListener(ServiceCoordinate.parse(SOME_COORDINATE), new ServiceListener() {
@Override
public void onServiceCreated(final InstanceCoordinate coordinate, final ServiceData data) {
fail("Should not have any services but " + coordinate + " is still there");
}
@Override
public void onServiceDataChanged(final InstanceCoordinate coordinate, final ServiceData data) {
fail("Should not have any services but " + coordinate + " reports data");
}
@Override
public void onServiceRemoved(final InstanceCoordinate coordinate) {
}
});
mainCloudname.addServiceListener(ServiceCoordinate.parse(ANOTHER_COORDINATE), new ServiceListener() {
@Override
public void onServiceCreated(final InstanceCoordinate coordinate, final ServiceData data) {
fail("Should not have any services but " + coordinate + " is still there");
}
@Override
public void onServiceDataChanged(final InstanceCoordinate coordinate, final ServiceData data) {
fail("Should not have any services but " + coordinate + " is still there");
}
@Override
public void onServiceRemoved(InstanceCoordinate coordinate) {
}
});
}
/**
* Ensure data notifications works as expecte. Update a lot of endpoints on a single
* service and check that the subscribers get notified of all changes in the correct order.
*/
@Test
public void testDataNotifications() throws InterruptedException {
final CloudnameService cs = new CloudnameService(memoryBackend);
final String serviceCoordinate = "some.service.name";
final ServiceHandle serviceHandle = cs.registerService(
ServiceCoordinate.parse(serviceCoordinate),
new ServiceData(new ArrayList<Endpoint>()));
final int numClients = 10;
final int numDataChanges = 50;
final int maxSecondsForNotifications = 1;
final CountDownLatch dataChangeLatch = new CountDownLatch(numClients * numDataChanges);
final CountDownLatch readyLatch = new CountDownLatch(numClients);
final String EP_NAME = "endpoint";
final Semaphore terminateSemaphore = new Semaphore(1);
// Grab the semaphore, prevent threads from completing
terminateSemaphore.acquire();
final Runnable clientServices = new Runnable() {
@Override
public void run() {
try (final CloudnameService cn = new CloudnameService(memoryBackend)) {
cn.addServiceListener(ServiceCoordinate.parse(serviceCoordinate), new ServiceListener() {
int portNum = 0;
@Override
public void onServiceCreated(InstanceCoordinate coordinate, ServiceData serviceData) {
// ignore this
}
@Override
public void onServiceDataChanged(InstanceCoordinate coordinate, ServiceData data) {
final Endpoint ep = data.getEndpoint(EP_NAME);
if (ep != null) {
dataChangeLatch.countDown();
assertThat(ep.getPort(), is(portNum + 1));
portNum = portNum + 1;
}
}
@Override
public void onServiceRemoved(InstanceCoordinate coordinate) {
// ignore this
}
});
readyLatch.countDown();
// Wait for the test to finish before closing. The endpoints will be
// processed once every thread is ready.
try {
terminateSemaphore.acquire();
terminateSemaphore.release();
} catch (final InterruptedException ie) {
throw new RuntimeException(ie);
}
}
}
};
final Executor executor = Executors.newCachedThreadPool();
for (int i = 0; i < numClients; i++) {
executor.execute(clientServices);
}
// Wait for the threads to be ready
readyLatch.await();
// Publish changes to the same endpoint; the endpoint is updated with a new port
// number for each update.
Endpoint oldEndpoint = null;
for (int portNum = 1; portNum < numDataChanges + 1; portNum++) {
if (oldEndpoint != null) {
serviceHandle.removeEndpoint(oldEndpoint);
}
final Endpoint newEndpoint = new Endpoint(EP_NAME, "localhost", portNum);
serviceHandle.registerEndpoint(newEndpoint);
oldEndpoint = newEndpoint;
}
// Check if the threads have been notified of all the changes
assertTrue("Expected " + (numDataChanges * numClients) + " changes but "
+ dataChangeLatch.getCount() + " remains",
dataChangeLatch.await(maxSecondsForNotifications, TimeUnit.SECONDS));
// Let threads terminate
terminateSemaphore.release();
}
@Test(expected = IllegalArgumentException.class)
public void coordinateCanNotBeNullWhenAddingListener() {
new CloudnameService(memoryBackend).addServiceListener(null, null);
}
@Test(expected = IllegalArgumentException.class)
public void listenerCanNotBeNullWhenAddingListener() {
new CloudnameService(memoryBackend).addServiceListener(coordinate, null);
}
@Test(expected = IllegalArgumentException.class)
public void serviceCannotBeNullWhenRegister() {
new CloudnameService(memoryBackend).registerService(null, null);
}
@Test(expected = IllegalArgumentException.class)
public void serviceDataCannotBeNullWhenRegister() {
new CloudnameService(memoryBackend).registerService(coordinate, null);
}
@Test(expected = IllegalArgumentException.class)
public void backendMustBeValid() {
new CloudnameService(null);
}
}