wave 2 of refactoring ConcurrentOpenSocketFinder and test

This commit is contained in:
Adrian Cole 2012-12-03 21:52:25 -08:00
parent 35795d3bfc
commit 472f6dcc32
2 changed files with 138 additions and 122 deletions

View File

@ -18,28 +18,27 @@
*/ */
package org.jclouds.compute.util; package org.jclouds.compute.util;
import static java.lang.String.format;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Predicates.or;
import static com.google.common.base.Throwables.propagate; import static com.google.common.base.Throwables.propagate;
import static com.google.common.collect.Iterables.concat; import static com.google.common.collect.Iterables.concat;
import static com.google.common.collect.Iterables.size; import static com.google.common.collect.Iterables.size;
import static com.google.common.util.concurrent.Atomics.newReference; import static com.google.common.util.concurrent.Atomics.newReference;
import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator; import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator;
import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor; import static org.jclouds.Constants.PROPERTY_USER_THREADS;
import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_RUNNING; import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_RUNNING;
import java.util.Collection;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.inject.Named; import javax.inject.Named;
import org.jclouds.Constants;
import org.jclouds.compute.domain.NodeMetadata; import org.jclouds.compute.domain.NodeMetadata;
import org.jclouds.compute.reference.ComputeServiceConstants; import org.jclouds.compute.reference.ComputeServiceConstants;
import org.jclouds.logging.Logger; import org.jclouds.logging.Logger;
@ -50,13 +49,16 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function; import com.google.common.base.Function;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable; import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.net.HostAndPort; import com.google.common.net.HostAndPort;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.inject.Inject; import com.google.inject.Inject;
public final class ConcurrentOpenSocketFinder implements OpenSocketFinder { public class ConcurrentOpenSocketFinder implements OpenSocketFinder {
@Resource @Resource
@Named(ComputeServiceConstants.COMPUTE_LOGGER) @Named(ComputeServiceConstants.COMPUTE_LOGGER)
@ -70,16 +72,15 @@ public final class ConcurrentOpenSocketFinder implements OpenSocketFinder {
@VisibleForTesting @VisibleForTesting
ConcurrentOpenSocketFinder(SocketOpen socketTester, ConcurrentOpenSocketFinder(SocketOpen socketTester,
@Named(TIMEOUT_NODE_RUNNING) Predicate<AtomicReference<NodeMetadata>> nodeRunning, @Named(TIMEOUT_NODE_RUNNING) Predicate<AtomicReference<NodeMetadata>> nodeRunning,
@Named(Constants.PROPERTY_USER_THREADS) ExecutorService userThreads) { @Named(PROPERTY_USER_THREADS) ExecutorService userThreads) {
this.socketTester =checkNotNull(socketTester, "socketTester"); this.socketTester = checkNotNull(socketTester, "socketTester");
this.nodeRunning = checkNotNull(nodeRunning, "nodeRunning"); this.nodeRunning = checkNotNull(nodeRunning, "nodeRunning");
this.executor = listeningDecorator(checkNotNull(userThreads, "userThreads")); this.executor = listeningDecorator(checkNotNull(userThreads, "userThreads"));
} }
public HostAndPort findOpenSocketOnNode(NodeMetadata node, final int port, @Override
long timeoutValue, TimeUnit timeUnits) { public HostAndPort findOpenSocketOnNode(NodeMetadata node, final int port, long timeoutValue, TimeUnit timeUnits) {
FluentIterable<String> hosts = checkNodeHasIps(node); ImmutableSet<HostAndPort> sockets = checkNodeHasIps(node).transform(new Function<String, HostAndPort>() {
ImmutableSet<HostAndPort> sockets = hosts.transform(new Function<String, HostAndPort>() {
@Override @Override
public HostAndPort apply(String from) { public HostAndPort apply(String from) {
@ -90,34 +91,13 @@ public final class ConcurrentOpenSocketFinder implements OpenSocketFinder {
// Specify a retry period of 1s, expressed in the same time units. // Specify a retry period of 1s, expressed in the same time units.
long period = timeUnits.convert(1, TimeUnit.SECONDS); long period = timeUnits.convert(1, TimeUnit.SECONDS);
// For storing the result; needed because predicate will just tell us true/false // For retrieving the socket found (if any)
final AtomicReference<HostAndPort> result = newReference(); AtomicReference<HostAndPort> result = newReference();
final AtomicReference<NodeMetadata> nodeReference = newReference(node);
Predicate<Collection<HostAndPort>> concurrentOpenSocketFinder = new Predicate<Collection<HostAndPort>>() { Predicate<Iterable<HostAndPort>> findOrBreak = or(updateRefOnSocketOpen(result), throwISEIfNoLongerRunning(node));
@Override
public boolean apply(Collection<HostAndPort> input) {
HostAndPort reachableSocket = findOpenSocket(input);
if (reachableSocket != null) {
result.set(reachableSocket);
return true;
} else {
if (!nodeRunning.apply(nodeReference)) {
throw new IllegalStateException(String.format("Node %s is no longer running; aborting waiting for ip:port connection", nodeReference.get().getId()));
}
return false;
}
}
};
RetryablePredicate<Collection<HostAndPort>> retryingOpenSocketFinder = new RetryablePredicate<Collection<HostAndPort>>(
concurrentOpenSocketFinder, timeoutValue, period, timeUnits);
logger.debug(">> blocking on sockets %s for %d %s", sockets, timeoutValue, timeUnits); logger.debug(">> blocking on sockets %s for %d %s", sockets, timeoutValue, timeUnits);
boolean passed = retryPredicate(findOrBreak, period, timeoutValue, timeUnits).apply(sockets);
boolean passed = retryingOpenSocketFinder.apply(sockets);
if (passed) { if (passed) {
logger.debug("<< socket %s opened", result); logger.debug("<< socket %s opened", result);
@ -125,66 +105,99 @@ public final class ConcurrentOpenSocketFinder implements OpenSocketFinder {
return result.get(); return result.get();
} else { } else {
logger.warn("<< sockets %s didn't open after %d %s", sockets, timeoutValue, timeUnits); logger.warn("<< sockets %s didn't open after %d %s", sockets, timeoutValue, timeUnits);
throw new NoSuchElementException(String.format("could not connect to any ip address port %d on node %s", throw new NoSuchElementException(format("could not connect to any ip address port %d on node %s", port, node));
port, node));
} }
} }
/** /**
* Checks if any any of the given HostAndPorts are reachable. It checks them all concurrently, * Checks if any any of the given HostAndPorts are reachable. It checks them
* and returns the first one found or null if none are reachable. * all concurrently, and sets reference to a {@link HostAndPort} if found or
* * returns false;
* @return A reachable HostAndPort, or null.
* @throws InterruptedException
*/ */
private HostAndPort findOpenSocket(final Collection<HostAndPort> sockets) { private Predicate<Iterable<HostAndPort>> updateRefOnSocketOpen(final AtomicReference<HostAndPort> reachableSocket) {
final AtomicReference<HostAndPort> result = newReference(); return new Predicate<Iterable<HostAndPort>>() {
final CountDownLatch latch = new CountDownLatch(1);
final AtomicInteger completeCount = new AtomicInteger();
for (final HostAndPort socket : sockets) { @Override
final ListenableFuture<?> future = executor.submit(new Runnable() { public boolean apply(Iterable<HostAndPort> input) {
Builder<ListenableFuture<?>> futures = ImmutableList.builder();
for (final HostAndPort socket : input) {
futures.add(executor.submit(new Runnable() {
@Override @Override
public void run() { public void run() {
try { try {
if (socketTester.apply(socket)) { if (socketTester.apply(socket)) {
result.compareAndSet(null, socket); // only set if the this socket was found first
latch.countDown(); reachableSocket.compareAndSet(null, socket);
} }
} catch (RuntimeException e) { } catch (RuntimeException e) {
logger.warn(e, "Error checking reachability of ip:port %s", socket); logger.warn(e, "Error checking reachability of ip:port %s", socket);
} }
} }
}); }));
}
future.addListener(new Runnable() { blockOn(futures.build());
return reachableSocket.get() != null;
}
@Override @Override
public void run() { public String toString() {
if (completeCount.incrementAndGet() >= sockets.size()) { return "setAndReturnTrueIfSocketFound()";
latch.countDown(); // Tried all; mark as done
} }
};
} }
}, sameThreadExecutor()); /**
* Add this via
* {@code Predicates.or(condition, throwISEIfNoLongerRunning(node))} to
* short-circuit {@link RetryablePredicate} looping when the node is no
* longer running.
*/
private <T> Predicate<T> throwISEIfNoLongerRunning(final NodeMetadata node) {
return new Predicate<T>() {
@Override
public boolean apply(T input) {
if (!nodeRunning.apply(newReference(node))) {
throw new IllegalStateException(node.getId() + " is no longer running; aborting socket open loop");
}
return false;
} }
try { @Override
latch.await(); public String toString() {
} catch (InterruptedException e) { return "throwISEIfNoLongerRunning(" + node.getId() + ")";
Thread.currentThread().interrupt();
throw propagate(e);
} }
return result.get(); };
} }
private FluentIterable<String> checkNodeHasIps(NodeMetadata node) { /**
* @param findOrBreak
* throws {@link IllegalStateException} in order to break the retry
* loop
*/
@VisibleForTesting
<T> Predicate<T> retryPredicate(Predicate<T> findOrBreak, long period, long timeoutValue, TimeUnit timeUnits) {
return new RetryablePredicate<T>(findOrBreak, timeoutValue, period, timeUnits);
}
private static FluentIterable<String> checkNodeHasIps(NodeMetadata node) {
FluentIterable<String> ips = FluentIterable.from(concat(node.getPublicAddresses(), node.getPrivateAddresses())); FluentIterable<String> ips = FluentIterable.from(concat(node.getPublicAddresses(), node.getPrivateAddresses()));
checkState(size(ips) > 0, "node does not have IP addresses configured: " + node); checkState(size(ips) > 0, "node does not have IP addresses configured: " + node);
return ips; return ips;
} }
private static void blockOn(Iterable<ListenableFuture<?>> immutableList) {
try {
Futures.allAsList(immutableList).get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw propagate(e);
} catch (ExecutionException e) {
throw propagate(e);
}
}
} }

View File

@ -23,10 +23,6 @@ import static com.google.common.base.Predicates.alwaysTrue;
import static com.google.common.base.Throwables.propagate; import static com.google.common.base.Throwables.propagate;
import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;
import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue; import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail; import static org.testng.Assert.fail;
@ -36,14 +32,14 @@ import java.util.NoSuchElementException;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import org.easymock.EasyMock;
import org.jclouds.compute.domain.NodeMetadata; import org.jclouds.compute.domain.NodeMetadata;
import org.jclouds.compute.domain.NodeMetadataBuilder; import org.jclouds.compute.domain.NodeMetadataBuilder;
import org.jclouds.predicates.SocketOpen; import org.jclouds.predicates.SocketOpen;
import org.testng.annotations.AfterMethod; import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test; import org.testng.annotations.Test;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
@ -61,19 +57,24 @@ public class ConcurrentOpenSocketFinderTest {
private final NodeMetadata node = new NodeMetadataBuilder().id("myid").status(NodeMetadata.Status.RUNNING) private final NodeMetadata node = new NodeMetadataBuilder().id("myid").status(NodeMetadata.Status.RUNNING)
.publicAddresses(ImmutableSet.of("1.2.3.4")).privateAddresses(ImmutableSet.of("1.2.3.5")).build(); .publicAddresses(ImmutableSet.of("1.2.3.4")).privateAddresses(ImmutableSet.of("1.2.3.5")).build();
private final Predicate<AtomicReference<NodeMetadata>> alwaysTrue = alwaysTrue(); private final SocketOpen socketAlwaysClosed = new SocketOpen() {
private final Predicate<AtomicReference<NodeMetadata>> alwaysFalse = alwaysFalse(); @Override
public boolean apply(HostAndPort input) {
return false;
}
};
private final Predicate<AtomicReference<NodeMetadata>> nodeRunning = alwaysTrue();
private final Predicate<AtomicReference<NodeMetadata>> nodeNotRunning = alwaysFalse();
private SocketOpen socketTester;
private ExecutorService threadPool; private ExecutorService threadPool;
@BeforeMethod @BeforeClass
public void setUp() { public void setUp() {
socketTester = createMock(SocketOpen.class);
threadPool = Executors.newCachedThreadPool(); threadPool = Executors.newCachedThreadPool();
} }
@AfterMethod(alwaysRun = true) @AfterClass(alwaysRun = true)
public void tearDown() { public void tearDown() {
if (threadPool != null) if (threadPool != null)
threadPool.shutdownNow(); threadPool.shutdownNow();
@ -83,11 +84,7 @@ public class ConcurrentOpenSocketFinderTest {
public void testRespectsTimeout() throws Exception { public void testRespectsTimeout() throws Exception {
final long timeoutMs = 1000; final long timeoutMs = 1000;
expect(socketTester.apply(HostAndPort.fromParts("1.2.3.4", 22))).andReturn(false).times(2, Integer.MAX_VALUE); OpenSocketFinder finder = new ConcurrentOpenSocketFinder(socketAlwaysClosed, nodeRunning, threadPool);
expect(socketTester.apply(HostAndPort.fromParts("1.2.3.5", 22))).andReturn(false).times(2, Integer.MAX_VALUE);
replay(socketTester);
OpenSocketFinder finder = new ConcurrentOpenSocketFinder(socketTester, alwaysTrue, threadPool);
Stopwatch stopwatch = new Stopwatch(); Stopwatch stopwatch = new Stopwatch();
stopwatch.start(); stopwatch.start();
@ -101,32 +98,31 @@ public class ConcurrentOpenSocketFinderTest {
assertTrue(timetaken >= timeoutMs - EARLY_GRACE && timetaken <= timeoutMs + SLOW_GRACE, "timetaken=" + timetaken); assertTrue(timetaken >= timeoutMs - EARLY_GRACE && timetaken <= timeoutMs + SLOW_GRACE, "timetaken=" + timetaken);
verify(socketTester);
} }
@Test @Test
public void testReturnsReachable() throws Exception { public void testReturnsReachable() throws Exception {
expect(socketTester.apply(HostAndPort.fromParts("1.2.3.4", 22))).andReturn(false).once(); SocketOpen secondSocketOpen = new SocketOpen() {
expect(socketTester.apply(HostAndPort.fromParts("1.2.3.5", 22))).andReturn(true).once(); @Override
replay(socketTester); public boolean apply(HostAndPort input) {
return HostAndPort.fromParts("1.2.3.5", 22).equals(input);
}
};
OpenSocketFinder finder = new ConcurrentOpenSocketFinder(socketTester, alwaysTrue, threadPool); OpenSocketFinder finder = new ConcurrentOpenSocketFinder(secondSocketOpen, nodeRunning, threadPool);
HostAndPort result = finder.findOpenSocketOnNode(node, 22, 2000, MILLISECONDS); HostAndPort result = finder.findOpenSocketOnNode(node, 22, 2000, MILLISECONDS);
assertEquals(result, HostAndPort.fromParts("1.2.3.5", 22)); assertEquals(result, HostAndPort.fromParts("1.2.3.5", 22));
verify(socketTester);
} }
@Test @Test
public void testChecksSocketsConcurrently() throws Exception { public void testChecksSocketsConcurrently() throws Exception {
// Can't use mock+answer for concurrency tests; EasyMock uses lock in
// ReplayState
ControllableSocketOpen socketTester = new ControllableSocketOpen(ImmutableMap.of( ControllableSocketOpen socketTester = new ControllableSocketOpen(ImmutableMap.of(
HostAndPort.fromParts("1.2.3.4", 22), new SlowCallable<Boolean>(true, 1500), HostAndPort.fromParts("1.2.3.4", 22), new SlowCallable<Boolean>(true, 1500),
HostAndPort.fromParts("1.2.3.5", 22), new SlowCallable<Boolean>(true, 1000))); HostAndPort.fromParts("1.2.3.5", 22), new SlowCallable<Boolean>(true, 1000)));
OpenSocketFinder finder = new ConcurrentOpenSocketFinder(socketTester, alwaysTrue, threadPool); OpenSocketFinder finder = new ConcurrentOpenSocketFinder(socketTester, nodeRunning, threadPool);
HostAndPort result = finder.findOpenSocketOnNode(node, 22, 2000, MILLISECONDS); HostAndPort result = finder.findOpenSocketOnNode(node, 22, 2000, MILLISECONDS);
assertEquals(result, HostAndPort.fromParts("1.2.3.5", 22)); assertEquals(result, HostAndPort.fromParts("1.2.3.5", 22));
@ -134,13 +130,25 @@ public class ConcurrentOpenSocketFinderTest {
@Test @Test
public void testAbortsWhenNodeNotRunning() throws Exception { public void testAbortsWhenNodeNotRunning() throws Exception {
expect(socketTester.apply(EasyMock.<HostAndPort> anyObject())).andReturn(false);
replay(socketTester);
OpenSocketFinder finder = new ConcurrentOpenSocketFinder(socketTester, alwaysFalse, threadPool); OpenSocketFinder finder = new ConcurrentOpenSocketFinder(socketAlwaysClosed, nodeNotRunning, threadPool) {
@Override
protected <T> Predicate<T> retryPredicate(final Predicate<T> findOrBreak, long period, long timeoutValue,
TimeUnit timeUnits) {
return new Predicate<T>() {
@Override
public boolean apply(T input) {
try {
findOrBreak.apply(input);
fail("should have thrown IllegalStateException");
} catch (IllegalStateException e) {
}
return false;
}
};
}
};
Stopwatch stopwatch = new Stopwatch();
stopwatch.start();
try { try {
finder.findOpenSocketOnNode(node, 22, 2000, MILLISECONDS); finder.findOpenSocketOnNode(node, 22, 2000, MILLISECONDS);
fail(); fail();
@ -149,11 +157,6 @@ public class ConcurrentOpenSocketFinderTest {
// Note: don't get the "no longer running" message, because // Note: don't get the "no longer running" message, because
// logged+swallowed by RetryablePredicate // logged+swallowed by RetryablePredicate
} }
long timetaken = stopwatch.elapsedMillis();
assertTrue(timetaken <= SLOW_GRACE, "timetaken=" + timetaken);
verify(socketTester);
} }
private static class SlowCallable<T> implements Callable<T> { private static class SlowCallable<T> implements Callable<T> {