/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License 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 org.apache.brooklyn.util.net;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.fail;
import java.net.ServerSocket;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.javalang.JavaClassNames;
import org.apache.brooklyn.util.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.net.HostAndPort;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
public class ReachableSocketFinderTest {
private static final Logger LOG = LoggerFactory.getLogger(ReachableSocketFinderTest.class);
private HostAndPort socket1;
private HostAndPort socket2;
private Map<HostAndPort, Boolean> reachabilityResults;
private ListeningExecutorService executor;
private Predicate<HostAndPort> socketTester;
private ReachableSocketFinder finder;
@BeforeMethod(alwaysRun=true)
public void setUp() throws Exception {
socket1 = HostAndPort.fromParts("1.1.1.1", 1111);
socket2 = HostAndPort.fromParts("1.1.1.2", 1112);
reachabilityResults = Maps.newConcurrentMap();
executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
socketTester = new Predicate<HostAndPort>() {
@Override public boolean apply(HostAndPort input) {
return Boolean.TRUE.equals(reachabilityResults.get(input));
}
};
finder = new ReachableSocketFinder(socketTester, executor);
}
@AfterMethod(alwaysRun=true)
public void tearDown() throws Exception {
if (executor != null) executor.shutdownNow();
}
@Test(expectedExceptions=IllegalStateException.class)
public void testWhenNoSocketsThrowsIllegalState() throws Exception {
finder.findOpenSocketOnNode(ImmutableList.<HostAndPort>of(), Duration.TEN_SECONDS);
}
@Test
public void testReturnsReachableSocket() throws Exception {
reachabilityResults.put(socket1, true);
reachabilityResults.put(socket2, false);
assertEquals(finder.findOpenSocketOnNode(ImmutableList.<HostAndPort>of(socket1, socket2), Duration.TEN_SECONDS), socket1);
reachabilityResults.put(socket1, false);
reachabilityResults.put(socket2, true);
assertEquals(finder.findOpenSocketOnNode(ImmutableList.<HostAndPort>of(socket1, socket2), Duration.TEN_SECONDS), socket2);
}
@Test
public void testPollsUntilPortReachable() throws Exception {
reachabilityResults.put(socket1, false);
reachabilityResults.put(socket2, false);
final ListenableFuture<HostAndPort> future = executor.submit(new Callable<HostAndPort>() {
@Override public HostAndPort call() throws Exception {
return finder.findOpenSocketOnNode(ImmutableList.<HostAndPort>of(socket1, socket2), Duration.TEN_SECONDS);
}});
// Should keep trying
Asserts.succeedsContinually(new Runnable() {
@Override public void run() {
assertFalse(future.isDone());
}});
// When port is reached, it completes
reachabilityResults.put(socket1, true);
assertEquals(future.get(30, TimeUnit.SECONDS), socket1);
}
// Mark as integration, as can't rely (in Apache infra) for a port to stay unused during test!
@Test(groups="Integration")
public void testReturnsRealReachableSocket() throws Exception {
ReachableSocketFinder realFinder = new ReachableSocketFinder(executor);
ServerSocket socket = connectToPort();
try {
HostAndPort addr = HostAndPort.fromParts(socket.getInetAddress().getHostAddress(), socket.getLocalPort());
HostAndPort wrongAddr = HostAndPort.fromParts(socket.getInetAddress().getHostAddress(), findAvailablePort());
assertEquals(realFinder.findOpenSocketOnNode(ImmutableList.of(addr, wrongAddr), Duration.ONE_MINUTE), addr);
} finally {
if (socket != null) {
socket.close();
}
}
}
// Mark as integration, as can't rely (in Apache infra) for a port to stay unused during test!
// And slow test - takes 5 seconds.
@Test(groups="Integration")
public void testFailsIfRealSocketUnreachable() throws Exception {
ReachableSocketFinder realFinder = new ReachableSocketFinder(executor);
HostAndPort wrongAddr = HostAndPort.fromParts(Networking.getLocalHost().getHostAddress(), findAvailablePort());
try {
HostAndPort result = realFinder.findOpenSocketOnNode(ImmutableList.of(wrongAddr), Duration.FIVE_SECONDS);
fail("Expected failure, but got "+result);
} catch (NoSuchElementException e) {
// success
}
}
private ServerSocket connectToPort() throws Exception {
ServerSocket result = new ServerSocket(0);
LOG.info("Acquired port "+result+" for test "+JavaClassNames.niceClassAndMethod());
return result;
}
private int findAvailablePort() throws Exception {
final int startPort = 58767;
final int endPort = 60000;
int port = startPort;
do {
if (Networking.isPortAvailable(port)) {
return port;
}
port++;
// repeat until we can get a port
} while (port <= endPort);
throw new Exception("could not get a port in range "+startPort+"-"+endPort);
}
}