Browse Source

Remove cluster tests

tags/6.7-RC1
Simon Brandhof 6 years ago
parent
commit
9e4fe4aca0

+ 0
- 3
server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java View File

@@ -78,7 +78,6 @@ import org.sonar.process.NetworkUtilsImpl;
import org.sonar.process.ProcessProperties;
import org.sonar.process.Props;
import org.sonar.process.logging.LogbackHelper;
import org.sonar.server.cluster.StartableHazelcastMember;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.component.index.ComponentIndexer;
import org.sonar.server.computation.task.projectanalysis.ProjectAnalysisTaskModule;
@@ -426,8 +425,6 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer {

if (props.valueAsBoolean(ProcessProperties.CLUSTER_ENABLED)) {
container.add(
StartableHazelcastMember.class,

// system health
CeDistributedInformationImpl.class,


+ 2
- 2
server/sonar-ce/src/test/java/org/sonar/ce/CeDistributedInformationImplTest.java View File

@@ -27,7 +27,7 @@ import java.util.Map;
import java.util.Set;
import org.junit.Test;
import org.sonar.ce.taskprocessor.CeWorkerFactory;
import org.sonar.server.cluster.StartableHazelcastMember;
import org.sonar.process.cluster.hz.HazelcastMember;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.MapEntry.entry;
@@ -45,7 +45,7 @@ public class CeDistributedInformationImplTest {
clientUUID3, ImmutableSet.of("4", "5", "6")
);

private StartableHazelcastMember hzClientWrapper = mock(StartableHazelcastMember.class);
private HazelcastMember hzClientWrapper = mock(HazelcastMember.class);

@Test
public void getWorkerUUIDs_returns_union_of_workers_uuids_of_local_and_cluster_worker_uuids() {

+ 2
- 42
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java View File

@@ -21,13 +21,10 @@ package org.sonar.ce.container;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.util.Date;
import java.util.Optional;
import java.util.Properties;
import java.util.stream.Collectors;
import org.apache.commons.dbcp.BasicDataSource;
import org.hamcrest.CoreMatchers;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -42,23 +39,15 @@ import org.sonar.ce.CeDistributedInformationImpl;
import org.sonar.ce.StandaloneCeDistributedInformation;
import org.sonar.db.DbTester;
import org.sonar.db.property.PropertyDto;
import org.sonar.process.NetworkUtilsImpl;
import org.sonar.process.ProcessId;
import org.sonar.process.ProcessProperties;
import org.sonar.process.Props;
import org.sonar.server.cluster.StartableHazelcastMember;

import static java.lang.String.valueOf;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assume.assumeThat;
import static org.mockito.Mockito.mock;
import static org.sonar.process.ProcessEntryPoint.PROPERTY_PROCESS_INDEX;
import static org.sonar.process.ProcessEntryPoint.PROPERTY_PROCESS_KEY;
import static org.sonar.process.ProcessEntryPoint.PROPERTY_SHARED_PATH;
import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
import static org.sonar.process.ProcessProperties.CLUSTER_NODE_HOST;
import static org.sonar.process.ProcessProperties.CLUSTER_NODE_PORT;
import static org.sonar.process.ProcessProperties.CLUSTER_NODE_TYPE;
import static org.sonar.process.ProcessProperties.PATH_DATA;
import static org.sonar.process.ProcessProperties.PATH_HOME;
import static org.sonar.process.ProcessProperties.PATH_TEMP;
@@ -86,36 +75,7 @@ public class ComputeEngineContainerImplTest {
}

@Test
public void real_start_with_cluster() throws IOException {
Optional<InetAddress> localhost = NetworkUtilsImpl.INSTANCE.getLocalNonLoopbackIpv4Address();
// test is ignored if offline
assumeThat(localhost.isPresent(), CoreMatchers.is(true));

Properties properties = getProperties();
properties.setProperty(PROPERTY_PROCESS_KEY, ProcessId.COMPUTE_ENGINE.getKey());
properties.setProperty(CLUSTER_ENABLED, "true");
properties.setProperty(CLUSTER_NODE_TYPE, "application");
properties.setProperty(CLUSTER_NODE_HOST, localhost.get().getHostAddress());
properties.setProperty(CLUSTER_NODE_PORT, "" + NetworkUtilsImpl.INSTANCE.getNextAvailablePort(localhost.get()));

// required persisted properties
insertProperty(CoreProperties.SERVER_ID, "a_startup_id");
insertProperty(CoreProperties.SERVER_STARTTIME, DateUtils.formatDateTime(new Date()));

underTest
.start(new Props(properties));

MutablePicoContainer picoContainer = underTest.getComponentContainer().getPicoContainer();
assertThat(
picoContainer.getComponentAdapters().stream()
.map(ComponentAdapter::getComponentImplementation)
.collect(Collectors.toList())).contains((Class) StartableHazelcastMember.class,
(Class) CeDistributedInformationImpl.class);
underTest.stop();
}

@Test
public void real_start_without_cluster() throws IOException {
public void test_real_start() throws IOException {
Properties properties = getProperties();

// required persisted properties
@@ -160,7 +120,7 @@ public class ComputeEngineContainerImplTest {
assertThat(
picoContainer.getComponentAdapters().stream()
.map(ComponentAdapter::getComponentImplementation)
.collect(Collectors.toList())).doesNotContain((Class) StartableHazelcastMember.class,
.collect(Collectors.toList())).doesNotContain(
(Class) CeDistributedInformationImpl.class).contains(
(Class) StandaloneCeDistributedInformation.class);
assertThat(picoContainer.getParent().getParent().getParent().getParent()).isNull();

+ 0
- 156
server/sonar-server/src/main/java/org/sonar/server/cluster/StartableHazelcastMember.java View File

@@ -1,156 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.cluster;

import com.hazelcast.core.Cluster;
import com.hazelcast.core.IAtomicReference;
import com.hazelcast.core.MemberSelector;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import org.sonar.api.Startable;
import org.sonar.api.config.Configuration;
import org.sonar.process.NetworkUtils;
import org.sonar.process.ProcessId;
import org.sonar.process.ProcessProperties;
import org.sonar.process.cluster.NodeType;
import org.sonar.process.cluster.hz.DistributedAnswer;
import org.sonar.process.cluster.hz.DistributedCall;
import org.sonar.process.cluster.hz.HazelcastMember;
import org.sonar.process.cluster.hz.HazelcastMemberBuilder;

import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Objects.requireNonNull;
import static org.sonar.process.ProcessEntryPoint.PROPERTY_PROCESS_KEY;
import static org.sonar.process.ProcessProperties.CLUSTER_HOSTS;
import static org.sonar.process.ProcessProperties.CLUSTER_NODE_HOST;
import static org.sonar.process.ProcessProperties.CLUSTER_NODE_TYPE;

/**
* Implementation of {@link HazelcastMember} as used by Compute Engine and
* Web Server processes. It is configured by {@link Configuration}
* and its lifecycle is managed by picocontainer.
*/
public class StartableHazelcastMember implements HazelcastMember, Startable {

private final Configuration config;
private final NetworkUtils network;
private HazelcastMember member = null;

public StartableHazelcastMember(Configuration config, NetworkUtils network) {
this.config = config;
this.network = network;
}

@Override
public <E> IAtomicReference<E> getAtomicReference(String name) {
return nonNullMember().getAtomicReference(name);
}

@Override
public <E> Set<E> getSet(String name) {
return nonNullMember().getSet(name);
}

@Override
public <E> List<E> getList(String name) {
return nonNullMember().getList(name);
}

@Override
public <K, V> Map<K, V> getMap(String name) {
return nonNullMember().getMap(name);
}

@Override
public <K, V> Map<K, V> getReplicatedMap(String name) {
return nonNullMember().getReplicatedMap(name);
}

@Override
public String getUuid() {
return nonNullMember().getUuid();
}

@Override
public Set<String> getMemberUuids() {
return nonNullMember().getMemberUuids();
}

@Override
public Lock getLock(String name) {
return nonNullMember().getLock(name);
}

@Override
public long getClusterTime() {
return nonNullMember().getClusterTime();
}

@Override
public Cluster getCluster() {
return nonNullMember().getCluster();
}

@Override
public <T> DistributedAnswer<T> call(DistributedCall<T> callable, MemberSelector memberSelector, long timeoutMs)
throws InterruptedException {
return nonNullMember().call(callable, memberSelector, timeoutMs);
}

private HazelcastMember nonNullMember() {
return requireNonNull(member, "Hazelcast member not started");
}

@Override
public void close() {
if (member != null) {
member.close();
member = null;
}
}

@Override
public void start() {
String networkAddress = config.get(CLUSTER_NODE_HOST).orElseThrow(() -> new IllegalStateException("Missing node host"));
int freePort;
try {
freePort = network.getNextAvailablePort(network.toInetAddress(networkAddress));
} catch (UnknownHostException e) {
throw new IllegalStateException(format("Can not resolve address %s", networkAddress), e);
}
this.member = new HazelcastMemberBuilder()
.setNodeName(config.get(ProcessProperties.CLUSTER_NODE_NAME).orElseThrow(() -> new IllegalStateException("Missing node name")))
.setNodeType(NodeType.parse(config.get(CLUSTER_NODE_TYPE).orElseThrow(() -> new IllegalStateException("Missing node type"))))
.setPort(freePort)
.setProcessId(ProcessId.fromKey(config.get(PROPERTY_PROCESS_KEY).orElseThrow(() -> new IllegalStateException("Missing process key"))))
.setMembers(asList(config.getStringArray(CLUSTER_HOSTS)))
.setNetworkInterface(networkAddress)
.build();
}

@Override
public void stop() {
close();
}
}

+ 0
- 23
server/sonar-server/src/main/java/org/sonar/server/cluster/package-info.java View File

@@ -1,23 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
@ParametersAreNonnullByDefault
package org.sonar.server.cluster;

import javax.annotation.ParametersAreNonnullByDefault;

+ 0
- 2
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java View File

@@ -39,7 +39,6 @@ import org.sonar.server.authentication.LogOAuthWarning;
import org.sonar.server.batch.BatchWsModule;
import org.sonar.server.branch.BranchFeatureProxyImpl;
import org.sonar.server.ce.ws.CeWsModule;
import org.sonar.server.cluster.StartableHazelcastMember;
import org.sonar.server.component.ComponentCleanerService;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.component.ComponentService;
@@ -247,7 +246,6 @@ public class PlatformLevel4 extends PlatformLevel {
EsDbCompatibilityImpl.class);

addIfCluster(
StartableHazelcastMember.class,
NodeHealthModule.class,
ChangeLogLevelClusterService.class);
addIfStandalone(

+ 0
- 107
server/sonar-server/src/test/java/org/sonar/server/cluster/StartableHazelcastMemberTest.java View File

@@ -1,107 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.cluster;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.function.Supplier;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.process.NetworkUtils;
import org.sonar.process.NetworkUtilsImpl;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;

public class StartableHazelcastMemberTest {

@Rule
public ExpectedException expectedException = ExpectedException.none();

private MapSettings settings = new MapSettings();
private String loopback = InetAddress.getLoopbackAddress().getHostAddress();

@Test
public void start_initializes_hazelcast() {
completeValidSettings();
StartableHazelcastMember underTest = new StartableHazelcastMember(settings.asConfig(), NetworkUtilsImpl.INSTANCE);
verifyStopped(underTest);

underTest.start();

assertThat(underTest.getUuid()).isNotEmpty();
assertThat(underTest.getCluster().getMembers()).hasSize(1);
assertThat(underTest.getMemberUuids()).containsExactly(underTest.getUuid());
assertThat(underTest.getSet("foo")).isNotNull();
assertThat(underTest.getReplicatedMap("foo")).isNotNull();
assertThat(underTest.getAtomicReference("foo")).isNotNull();
assertThat(underTest.getList("foo")).isNotNull();
assertThat(underTest.getMap("foo")).isNotNull();
assertThat(underTest.getLock("foo")).isNotNull();
assertThat(underTest.getClusterTime()).isGreaterThan(0);

underTest.stop();

verifyStopped(underTest);
}

@Test
public void throw_ISE_if_host_for_random_port_cant_be_resolved() throws Exception{
NetworkUtils network = mock(NetworkUtils.class);
doThrow(new UnknownHostException("BOOM")).when(network).toInetAddress(anyString());
completeValidSettings();
StartableHazelcastMember underTest = new StartableHazelcastMember(settings.asConfig(), network);

expectedException.expect(IllegalStateException.class);
expectedException.expectMessage("Can not resolve address ");

underTest.start();

verifyStopped(underTest);
}

private void completeValidSettings() {
settings.setProperty("sonar.cluster.name", "foo");
settings.setProperty("sonar.cluster.node.host", loopback);
settings.setProperty("sonar.cluster.node.name", "bar");
settings.setProperty("sonar.cluster.node.type", "application");
settings.setProperty("process.key", "ce");
}

private static void verifyStopped(StartableHazelcastMember member) {
expectNpe(member::getMemberUuids);
expectNpe(member::getCluster);
expectNpe(member::getUuid);
}

private static void expectNpe(Supplier supplier) {
try {
supplier.get();
fail();
} catch (NullPointerException e) {
}
}

}

+ 0
- 2
tests/src/test/java/org/sonarqube/tests/Category5Suite.java View File

@@ -25,7 +25,6 @@ import org.sonarqube.tests.analysis.AnalysisEsResilienceTest;
import org.sonarqube.tests.authorisation.SystemPasscodeTest;
import org.sonarqube.tests.ce.CeShutdownTest;
import org.sonarqube.tests.ce.CeWorkersTest;
import org.sonarqube.tests.cluster.ClusterTest;
import org.sonarqube.tests.issue.IssueCreationDatePluginChangedTest;
import org.sonarqube.tests.qualityProfile.ActiveRuleEsResilienceTest;
import org.sonarqube.tests.qualityProfile.BuiltInQualityProfilesNotificationTest;
@@ -51,7 +50,6 @@ import org.sonarqube.tests.user.UserEsResilienceTest;
*/
@RunWith(Suite.class)
@Suite.SuiteClasses({
ClusterTest.class,
ServerSystemRestartingOrchestrator.class,
RestartTest.class,
SettingsTestRestartingOrchestrator.class,

+ 0
- 131
tests/src/test/java/org/sonarqube/tests/cluster/Cluster.java View File

@@ -1,131 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarqube.tests.cluster;

import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.OrchestratorBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.slf4j.LoggerFactory;
import util.ItUtils;

import static java.util.stream.Collectors.joining;

class Cluster implements AutoCloseable {

@Nullable
private final String clusterName;

private final List<Node> nodes = new ArrayList<>();
private final String systemPassCode = "fooBar2000";

Cluster(@Nullable String name) {
this.clusterName = name;
}

Node startNode(NodeConfig config, Consumer<OrchestratorBuilder> consumer) {
Node node = addNode(config, consumer);
node.start();
return node;
}

Node addNode(NodeConfig config, Consumer<OrchestratorBuilder> consumer) {
OrchestratorBuilder builder = newOrchestratorBuilder(config);
builder.setServerProperty("sonar.web.systemPasscode", systemPassCode);

switch (config.getType()) {
case SEARCH:
builder
.setServerProperty("sonar.cluster.node.type", "search")
.setServerProperty("sonar.search.host", config.getAddress().getHostAddress())
.setServerProperty("sonar.search.port", "" + config.getSearchPort().get())
.setServerProperty("sonar.search.javaOpts", "-Xmx64m -Xms64m -XX:+HeapDumpOnOutOfMemoryError");
break;
case APPLICATION:
builder
.setServerProperty("sonar.cluster.node.type", "application")
.setServerProperty("sonar.web.host", config.getAddress().getHostAddress())
.setServerProperty("sonar.web.port", "" + config.getWebPort().get())
.setServerProperty("sonar.web.javaOpts", "-Xmx128m -Xms16m -XX:+HeapDumpOnOutOfMemoryError")
.setServerProperty("sonar.auth.jwtBase64Hs256Secret", "HrPSavOYLNNrwTY+SOqpChr7OwvbR/zbDLdVXRN0+Eg=")
.setServerProperty("sonar.ce.javaOpts", "-Xmx32m -Xms16m -XX:+HeapDumpOnOutOfMemoryError");
break;
}
consumer.accept(builder);
Orchestrator orchestrator = builder.build();
Node node = new Node(config, orchestrator, systemPassCode);
nodes.add(node);
return node;
}

Stream<Node> getNodes() {
return nodes.stream();
}

Stream<Node> getAppNodes() {
return nodes.stream().filter(n -> n.getConfig().getType() == NodeConfig.NodeType.APPLICATION);
}

Node getAppNode(int index) {
return getAppNodes().skip(index).findFirst().orElseThrow(IllegalArgumentException::new);
}

Stream<Node> getSearchNodes() {
return nodes.stream().filter(n -> n.getConfig().getType() == NodeConfig.NodeType.SEARCH);
}

Node getSearchNode(int index) {
return getSearchNodes().skip(index).findFirst().orElseThrow(IllegalArgumentException::new);
}

@Override
public void close() throws Exception {
// nodes are stopped in order of creation
for (Node node : nodes) {
try {
node.stop();
} catch (Exception e) {
LoggerFactory.getLogger(getClass()).error("Fail to stop node", e);
}
}
}

private OrchestratorBuilder newOrchestratorBuilder(NodeConfig node) {
OrchestratorBuilder builder = Orchestrator.builderEnv();
builder.setOrchestratorProperty("orchestrator.keepDatabase", "true");
builder.setServerProperty("sonar.cluster.enabled", "true");
builder.setServerProperty("sonar.cluster.node.host", node.getAddress().getHostAddress());
builder.setServerProperty("sonar.cluster.node.port", "" + node.getHzPort());
builder.setServerProperty("sonar.cluster.hosts", node.getConnectedNodes().stream().map(NodeConfig::getHzHost).collect(joining(",")));
builder.setServerProperty("sonar.cluster.search.hosts", node.getSearchNodes().stream().map(NodeConfig::getSearchHost).collect(joining(",")));
if (clusterName != null) {
builder.setServerProperty("sonar.cluster.name", clusterName);
}
if (node.getName().isPresent()) {
builder.setServerProperty("sonar.cluster.node.name", node.getName().get());
}
builder.addPlugin(ItUtils.pluginArtifact("server-plugin"));
builder.setStartupLogWatcher(logLine -> true);
return builder;
}
}

+ 0
- 451
tests/src/test/java/org/sonarqube/tests/cluster/ClusterTest.java View File

@@ -1,451 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarqube.tests.cluster;

import com.google.gson.internal.LinkedTreeMap;
import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.OrchestratorBuilder;
import com.sonar.orchestrator.db.DefaultDatabase;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.stream.IntStream;
import org.apache.commons.io.FileUtils;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.DisableOnDebug;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.TestRule;
import org.junit.rules.Timeout;
import org.sonarqube.pageobjects.Navigation;
import org.sonarqube.pageobjects.SystemInfoPage;
import org.sonarqube.ws.WsSystem;
import org.sonarqube.ws.client.HttpException;
import org.sonarqube.ws.client.issue.SearchWsRequest;
import util.ItUtils;

import static com.google.common.base.Preconditions.checkState;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.sonarqube.tests.cluster.NodeConfig.newApplicationConfig;
import static org.sonarqube.tests.cluster.NodeConfig.newSearchConfig;

@Ignore
public class ClusterTest {

@Rule
public TestRule safeguard = new DisableOnDebug(Timeout.seconds(300));

@Rule
public ExpectedException expectedException = ExpectedException.none();

@Rule
public TemporaryFolder temp = new TemporaryFolder();

@BeforeClass
public static void initDbSchema() throws Exception {
Orchestrator orchestrator = Orchestrator.builderEnv()
// enforce (re-)creation of database schema
.setOrchestratorProperty("orchestrator.keepDatabase", "false")
.build();
DefaultDatabase db = new DefaultDatabase(orchestrator.getConfiguration());
checkState(!db.getClient().getDialect().equals("h2"), "H2 is not supported in cluster mode");
db.start();
db.stop();
}

@Test
public void test_high_availability_topology() throws Exception {
try (Cluster cluster = newCluster(3, 2)) {
cluster.getNodes().forEach(Node::start);

cluster.getAppNode(0).waitForHealthGreen();
cluster.getAppNodes().forEach(node -> assertThat(node.getStatus()).hasValue(WsSystem.Status.UP));

cluster.getNodes().forEach(node -> {
node.assertThatProcessesAreUp();
assertThat(node.anyLogsContain(" ERROR ")).isFalse();
assertThat(node.anyLogsContain("MessageException")).isFalse();
});

verifyGreenHealthOfNodes(cluster);

// verify that there's a single web startup leader
Node startupLeader = cluster.getAppNodes()
.filter(Node::isStartupLeader)
.reduce(singleElement())
.get();
assertThat(startupLeader.hasStartupLeaderOperations()).isTrue();
assertThat(startupLeader.hasCreatedSearchIndices()).isTrue();

// verify that the second app node is a startup follower
Node startupFollower = cluster.getAppNodes()
.filter(Node::isStartupFollower)
.reduce(singleElement())
.get();
assertThat(startupFollower.hasStartupLeaderOperations()).isFalse();
assertThat(startupFollower.hasCreatedSearchIndices()).isFalse();

cluster.getAppNodes().forEach(app -> {
// compute engine is being started when app node is already in status UP
app.waitForCeLogsContain("Compute Engine is operational");
assertThat(app.anyLogsContain("Process[ce] is up")).isTrue();
});

testSystemInfoPage(cluster, cluster.getAppNode(0));
testSystemInfoPage(cluster, cluster.getAppNode(1));
}
}

private void verifyGreenHealthOfNodes(Cluster cluster) {
WsSystem.HealthResponse health = cluster.getAppNode(0).getHealth().get();
cluster.getNodes().forEach(node -> {
WsSystem.Node healthNode = health.getNodes().getNodesList().stream()
.filter(n -> n.getPort() == node.getConfig().getHzPort())
.findFirst()
.orElseThrow(() -> new IllegalStateException("Node with port " + node.getConfig().getHzPort() + " not found in api/system/health"));
assertThat(healthNode.getStartedAt()).isNotEmpty();
assertThat(healthNode.getHost()).isNotEmpty();
assertThat(healthNode.getCausesCount()).isEqualTo(0);
assertThat(healthNode.getHealth()).isEqualTo(WsSystem.Health.GREEN);
});
}

private void testSystemInfoPage(Cluster cluster, Node node) {
Navigation nav = node.openBrowser().logIn().submitCredentials("admin");
node.wsClient().users().skipOnboardingTutorial();
SystemInfoPage page = nav.openSystemInfo();

page.getCardItem("System")
.shouldHaveHealth()
.shouldHaveMainSection()
.shouldHaveSection("Database")
.shouldHaveField("Database Version")
.shouldHaveSection("Search State")
.shouldHaveSection("Search Indexes")
.shouldNotHaveSection("Settings")
.shouldNotHaveSection("Plugins")
.shouldHaveField("High Availability")
.shouldNotHaveField("Official Distribution")
.shouldNotHaveField("Version")
.shouldNotHaveField("Logs Level")
.shouldNotHaveField("Health")
.shouldNotHaveField("Health Causes");

cluster.getNodes().forEach(clusterNodes -> {
page.shouldHaveCard(clusterNodes.getConfig().getName().get());
});

page.getCardItem(String.valueOf(node.getConfig().getName().get()))
.shouldHaveHealth()
.shouldHaveSection("System")
.shouldHaveSection("Database")
.shouldHaveSection("Web Logging")
.shouldHaveSection("Compute Engine Logging")
.shouldNotHaveSection("Settings")
.shouldHaveField("Version")
.shouldHaveField("Official Distribution")
.shouldHaveField("Processors")
.shouldNotHaveField("Health")
.shouldNotHaveField("Health Causes");

page.getCardItem(cluster.getSearchNode(0).getConfig().getName().get())
.shouldHaveHealth()
.shouldHaveSection("Search State")
.shouldHaveField("Disk Available")
.shouldHaveField("Max File Descriptors")
.shouldNotHaveField("Health")
.shouldNotHaveField("Health Causes");
}

@Test
public void minimal_cluster_is_2_search_and_1_application_nodes() throws Exception {
try (Cluster cluster = newCluster(2, 1)) {
cluster.getNodes().forEach(Node::start);

Node app = cluster.getAppNode(0);
app.waitForStatusUp();
app.waitForCeLogsContain("Compute Engine is operational");

app.waitForHealth(WsSystem.Health.YELLOW);
WsSystem.HealthResponse health = app.getHealth().orElseThrow(() -> new IllegalStateException("Health is not available"));
assertThat(health.getCausesList()).extracting(WsSystem.Cause::getMessage)
.contains("There should be at least three search nodes")
.contains("There should be at least two application nodes");

assertThat(app.isStartupLeader()).isTrue();
assertThat(app.hasStartupLeaderOperations()).isTrue();

cluster.getNodes().forEach(node -> {
assertThat(node.anyLogsContain(" ERROR ")).isFalse();
node.assertThatProcessesAreUp();
});
}
}

@Test
public void configuration_of_connection_to_other_nodes_can_be_non_exhaustive() throws Exception {
try (Cluster cluster = new Cluster(null)) {
NodeConfig searchConfig1 = newSearchConfig("Search 1");
NodeConfig searchConfig2 = newSearchConfig("Search 2");
NodeConfig appConfig = newApplicationConfig("App 1");

// HZ bus : app -> search 2 -> search1, which is not recommended at all !!!
searchConfig2.addConnectionToBus(searchConfig1);
appConfig.addConnectionToBus(searchConfig2);

// search1 is not configured to connect search2
// app is not configured to connect to search 1
// --> not recommended at all !!!
searchConfig2.addConnectionToSearch(searchConfig1);
appConfig.addConnectionToSearch(searchConfig2);

cluster.startNode(searchConfig1, nothing());
cluster.startNode(searchConfig2, nothing());
Node app = cluster.startNode(appConfig, nothing());

app.waitForStatusUp();
assertThat(app.isStartupLeader()).isTrue();
assertThat(app.hasStartupLeaderOperations()).isTrue();

// no errors
cluster.getNodes().forEach(node -> assertThat(node.anyLogsContain(" ERROR ")).isFalse());
}
}

@Test
public void cluster_name_can_be_overridden() throws Exception {
try (Cluster cluster = new Cluster("foo")) {
NodeConfig searchConfig1 = newSearchConfig("Search 1");
NodeConfig searchConfig2 = newSearchConfig("Search 2");
NodeConfig appConfig = newApplicationConfig("App 1");
NodeConfig.interconnectBus(searchConfig1, searchConfig2, appConfig);
NodeConfig.interconnectSearch(searchConfig1, searchConfig2, appConfig);

cluster.startNode(searchConfig1, nothing());
cluster.startNode(searchConfig2, nothing());
cluster.startNode(appConfig, nothing());

Node appNode = cluster.getAppNode(0);
appNode.waitForStatusUp();
}
}

@Test
public void node_fails_to_join_cluster_if_different_cluster_name() throws Exception {
try (Cluster cluster = new Cluster("foo")) {
NodeConfig searchConfig1 = newSearchConfig("Search 1");
NodeConfig searchConfig2 = newSearchConfig("Search 2");
NodeConfig.interconnectBus(searchConfig1, searchConfig2);
NodeConfig.interconnectSearch(searchConfig1, searchConfig2);
cluster.startNode(searchConfig1, nothing());
cluster.startNode(searchConfig2, nothing());

NodeConfig searchConfig3 = newSearchConfig("Search 3")
.addConnectionToSearch(searchConfig1)
.addConnectionToBus(searchConfig1, searchConfig2);
Node search3 = cluster.addNode(searchConfig3, b -> b
.setServerProperty("sonar.cluster.name", "bar")
.setStartupLogWatcher(logLine -> logLine.contains("SonarQube is up")));
try {
search3.start();
fail();
} catch (IllegalStateException e) {
assertThat(e).hasMessage("Server startup failure");
// TODO how to force process to write into sonar.log, even if sonar.log.console=true ?
// assertThat(search3.anyLogsContain("This node has a cluster name [bar], which does not match [foo] from the cluster")).isTrue();
}
}
}

@Test
public void restarting_all_application_nodes_elects_a_new_startup_leader() throws Exception {
// no need for 3 search nodes, 2 is enough for the test
try (Cluster cluster = newCluster(2, 2)) {
cluster.getNodes().forEach(Node::start);
cluster.getAppNodes().forEach(Node::waitForStatusUp);

// stop application nodes only
cluster.getAppNodes().forEach(app -> {
app.stop();
app.cleanUpLogs();
// logs are empty, no more possible to know if node was startup leader/follower
assertThat(app.isStartupLeader()).isFalse();
assertThat(app.isStartupFollower()).isFalse();
});

// restart application nodes
cluster.getAppNodes().forEach(Node::start);
cluster.getAppNodes().forEach(Node::waitForStatusUp);

// one app node is elected as startup leader. It does some initialization stuff,
// like registration of rules. Search indices already exist and are up-to-date.
Node startupLeader = cluster.getAppNodes()
.filter(Node::isStartupLeader)
.reduce(singleElement())
.get();
assertThat(startupLeader.hasStartupLeaderOperations()).isTrue();
assertThat(startupLeader.hasCreatedSearchIndices()).isFalse();

Node startupFollower = cluster.getAppNodes()
.filter(Node::isStartupFollower)
.reduce(singleElement())
.get();
assertThat(startupFollower.hasStartupLeaderOperations()).isFalse();
assertThat(startupFollower.hasCreatedSearchIndices()).isFalse();
assertThat(startupFollower).isNotSameAs(startupLeader);
}
}

@Test
public void set_log_level_affects_all_nodes() throws Exception {
try (Cluster cluster = newCluster(2, 2)) {
cluster.getNodes().forEach(Node::start);
cluster.getAppNodes().forEach(Node::waitForStatusUp);

cluster.getAppNodes().forEach(node -> {
assertThat(node.webLogsContain(" TRACE web[")).isFalse();
});

cluster.getAppNode(0).wsClient().system().changeLogLevel("TRACE");

cluster.getAppNodes().forEach(node -> {

// do something, that will produce logging
node.wsClient().issues().search(new SearchWsRequest());

// check logs
assertThat(node.webLogsContain(" TRACE web[")).isTrue();
});

Map<String, Object> data = ItUtils.jsonToMap(cluster.getAppNode(0).wsClient().system().info().content());
ArrayList<Object> applicationNodes = (ArrayList<Object>) data.get("Application Nodes");
applicationNodes.forEach(node -> {
LinkedTreeMap<Object, Object> nodeData = (LinkedTreeMap<Object, Object>) node;
LinkedTreeMap<Object, Object> ceLoggingData = (LinkedTreeMap<Object, Object>) nodeData.get("Compute Engine Logging");
assertThat(ceLoggingData.get("Logs Level")).as("Compute engine logs level of a node").isEqualTo("TRACE");
});
}
}

@Test
public void restart_action_is_not_allowed_for_cluster_nodes() throws Exception {
try (Cluster cluster = newCluster(2, 1)) {
cluster.getNodes().forEach(Node::start);
cluster.getAppNodes().forEach(Node::waitForStatusUp);

cluster.getAppNodes().forEach(node -> {
try {
node.wsClient().system().restart();
fail("The restart webservice must not succeed on cluster nodes");
} catch (HttpException e) {
// all good, we expected this!
assertThat(e.code()).isEqualTo(400);
assertThat(e.content()).contains("Restart not allowed for cluster nodes");
}
});
}
}

@Test
public void health_becomes_RED_when_all_search_nodes_go_down() throws Exception {
try (Cluster cluster = newCluster(2, 1)) {
cluster.getNodes().forEach(Node::start);

Node app = cluster.getAppNode(0);
app.waitForHealth(WsSystem.Health.YELLOW);

cluster.getSearchNodes().forEach(Node::stop);

app.waitForHealth(WsSystem.Health.RED);
assertThat(app.getHealth().get().getCausesList()).extracting(WsSystem.Cause::getMessage)
.contains("Elasticsearch status is RED (unavailable)");
}
}

@Test
public void health_ws_is_available_when_server_is_starting() throws Exception {
File startupLock = temp.newFile();
FileUtils.touch(startupLock);

try (Cluster cluster = newCluster(2, 0)) {
// add an application node that pauses during startup
NodeConfig appConfig = NodeConfig.newApplicationConfig("App 1")
.addConnectionToBus(cluster.getSearchNode(0).getConfig())
.addConnectionToSearch(cluster.getSearchNode(0).getConfig());
Node appNode = cluster.addNode(appConfig, b -> b.setServerProperty("sonar.web.startupLock.path", startupLock.getAbsolutePath()));

cluster.getNodes().forEach(Node::start);

appNode.waitFor(node -> WsSystem.Status.STARTING == node.getStatus().orElse(null));

// WS answers whereas server is still not started
assertThat(appNode.getHealth().get().getHealth()).isEqualTo(WsSystem.Health.RED);

// just to be sure, verify that server is still being started
assertThat(appNode.getStatus()).hasValue(WsSystem.Status.STARTING);

startupLock.delete();
}
}

/**
* Used to have non-blocking {@link Node#start()}. Orchestrator considers
* node to be up as soon as the first log is generated.
*/
private static Consumer<OrchestratorBuilder> nothing() {
return b -> {
};
}

/**
* Configure a cluster with recommended configuration (each node has references
* to other nodes)
*/
private static Cluster newCluster(int nbOfSearchNodes, int nbOfAppNodes) {
Cluster cluster = new Cluster(null);

List<NodeConfig> configs = new ArrayList<>();
IntStream.range(0, nbOfSearchNodes).forEach(i -> configs.add(newSearchConfig("Search " + i)));
IntStream.range(0, nbOfAppNodes).forEach(i -> configs.add(newApplicationConfig("App " + i)));
NodeConfig[] configsArray = configs.toArray(new NodeConfig[configs.size()]);

// a node is connected to all nodes, including itself (see sonar.cluster.hosts)
NodeConfig.interconnectBus(configsArray);

// search nodes are interconnected, and app nodes connect to all search nodes
NodeConfig.interconnectSearch(configsArray);

configs.forEach(c -> cluster.addNode(c, nothing()));
return cluster;
}

private static BinaryOperator<Node> singleElement() {
return (a, b) -> {
throw new IllegalStateException("More than one element");
};
}
}

+ 0
- 248
tests/src/test/java/org/sonarqube/tests/cluster/Node.java View File

@@ -1,248 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarqube.tests.cluster;

import com.google.common.util.concurrent.Uninterruptibles;
import com.sonar.orchestrator.Orchestrator;
import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import javax.annotation.Nullable;
import org.apache.commons.io.FileUtils;
import org.sonarqube.pageobjects.Navigation;
import org.sonarqube.tests.LogsTailer;
import org.sonarqube.ws.WsSystem;
import org.sonarqube.ws.client.WsClient;
import util.ItUtils;

import static com.google.common.base.Preconditions.checkState;
import static org.assertj.core.api.Assertions.assertThat;

class Node {

private final NodeConfig config;
private final Orchestrator orchestrator;
private final String systemPassCode;
private LogsTailer logsTailer;
private final LogsTailer.Content content = new LogsTailer.Content();

Node(NodeConfig config, Orchestrator orchestrator, String systemPassCode) {
this.config = config;
this.orchestrator = orchestrator;
this.systemPassCode = systemPassCode;
}

NodeConfig getConfig() {
return config;
}

/**
* Non-blocking startup of node. The method does not wait for
* node to be started because Orchestrator uses a StartupLogWatcher
* that returns as soon as a log is generated.
*/
void start() {
orchestrator.start();
logsTailer = LogsTailer.builder()
.addFile(orchestrator.getServer().getWebLogs())
.addFile(orchestrator.getServer().getCeLogs())
.addFile(orchestrator.getServer().getEsLogs())
.addFile(orchestrator.getServer().getAppLogs())
.addConsumer(content)
.build();
}

void stop() {
orchestrator.stop();
if (logsTailer != null) {
logsTailer.close();
}
}

void cleanUpLogs() {
if (orchestrator.getServer() != null) {
FileUtils.deleteQuietly(orchestrator.getServer().getWebLogs());
FileUtils.deleteQuietly(orchestrator.getServer().getCeLogs());
FileUtils.deleteQuietly(orchestrator.getServer().getEsLogs());
FileUtils.deleteQuietly(orchestrator.getServer().getAppLogs());
}
}

boolean isStartupLeader() {
return webLogsContain("Cluster enabled (startup leader)");
}

boolean isStartupFollower() {
return webLogsContain("Cluster enabled (startup follower)");
}

void waitForStatusUp() {
waitFor(node -> WsSystem.Status.UP == node.getStatus().orElse(null));
}

/**
* Waiting for health to be green... or yellow on the boxes that
* have less than 15% of free disk space. In that case Elasticsearch
* can't build shard replicas so it is yellow.
*/
void waitForHealthGreen() {
waitFor(node -> {
Optional<WsSystem.HealthResponse> health = node.getHealth();
if (!health.isPresent()) {
return false;
}
if (health.get().getHealth() == WsSystem.Health.GREEN) {
return true;
}
if (health.get().getHealth() == WsSystem.Health.YELLOW) {
List<WsSystem.Cause> causes = health.get().getCausesList();
return causes.size() == 1 && "Elasticsearch status is YELLOW".equals(causes.get(0).getMessage());
}
return false;
});
}

void waitForHealth(WsSystem.Health expectedHealth) {
waitFor(node -> expectedHealth.equals(node.getHealth().map(WsSystem.HealthResponse::getHealth).orElse(null)));
}

Optional<WsSystem.Status> getStatus() {
checkState(config.getType() == NodeConfig.NodeType.APPLICATION);
if (orchestrator.getServer() == null) {
return Optional.empty();
}
try {
return Optional.ofNullable(ItUtils.newAdminWsClient(orchestrator).system().status().getStatus());
} catch (Exception e) {
return Optional.empty();
}
}

Optional<WsSystem.HealthResponse> getHealth() {
checkState(config.getType() == NodeConfig.NodeType.APPLICATION);
if (orchestrator.getServer() == null) {
return Optional.empty();
}
try {
return Optional.ofNullable(ItUtils.newSystemUserWsClient(orchestrator, systemPassCode).system().health());
} catch (Exception e) {
return Optional.empty();
}
}

void waitFor(Predicate<Node> predicate) {
try {
while (!predicate.test(this)) {
Thread.sleep(500);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

void assertThatProcessesAreUp() {
assertThat(arePortsBound()).as(getConfig().getType().toString()).isTrue();
switch (config.getType()) {
case SEARCH:
assertThat(anyLogsContain("Process[es] is up")).isTrue();
assertThat(anyLogsContain("Process[web] is up")).isFalse();
assertThat(anyLogsContain("Elasticsearch cluster enabled")).isTrue();
break;
case APPLICATION:
assertThat(anyLogsContain("Process[es] is up")).isFalse();
assertThat(anyLogsContain("Process[web] is up")).isTrue();
assertThat(anyLogsContain("Elasticsearch cluster enabled")).isFalse();
break;
}
}

void waitForCeLogsContain(String expectedMessage) {
boolean found = false;
while (!found) {
found = orchestrator.getServer() != null && fileContains(orchestrator.getServer().getCeLogs(), expectedMessage);
if (!found) {
Uninterruptibles.sleepUninterruptibly(1_000, TimeUnit.MILLISECONDS);
}
}
}

boolean hasStartupLeaderOperations() throws IOException {
if (orchestrator.getServer() == null) {
return false;
}
String logs = FileUtils.readFileToString(orchestrator.getServer().getWebLogs());
return logs.contains("Register metrics") &&
logs.contains("Register rules");
}

boolean hasCreatedSearchIndices() throws IOException {
if (orchestrator.getServer() == null) {
return false;
}
String logs = FileUtils.readFileToString(orchestrator.getServer().getWebLogs());
return logs.contains("[o.s.s.e.IndexCreator] Create index");
}

boolean anyLogsContain(String message) {
return content.hasText(message);
}

boolean webLogsContain(String message) {
if (orchestrator.getServer() == null) {
return false;
}
return fileContains(orchestrator.getServer().getWebLogs(), message);
}

Navigation openBrowser() {
return Navigation.create(orchestrator);
}

private static boolean fileContains(@Nullable File logFile, String message) {
try {
return logFile != null && logFile.exists() && FileUtils.readFileToString(logFile).contains(message);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}

private boolean arePortsBound() {
return isPortBound(config.getHzPort()) &&
config.getSearchPort().map(this::isPortBound).orElse(true) &&
config.getWebPort().map(this::isPortBound).orElse(true);
}

private boolean isPortBound(int port) {
try (ServerSocket socket = new ServerSocket(port, 50, config.getAddress())) {
return false;
} catch (IOException e) {
return true;
}
}

public WsClient wsClient() {
checkState(config.getType() == NodeConfig.NodeType.APPLICATION);
return ItUtils.newAdminWsClient(orchestrator);
}
}

+ 0
- 187
tests/src/test/java/org/sonarqube/tests/cluster/NodeConfig.java View File

@@ -1,187 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarqube.tests.cluster;

import com.sonar.orchestrator.util.NetworkUtils;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;

import static com.google.common.base.Preconditions.checkArgument;

class NodeConfig {

enum NodeType {
SEARCH("search"), APPLICATION("application");

final String value;

NodeType(String value) {
this.value = value;
}

String getValue() {
return value;
}
}

private final NodeType type;
@Nullable
private final String name;
private final InetAddress address;
private final int hzPort;
@Nullable
private final Integer searchPort;
@Nullable
private final Integer webPort;
private final List<NodeConfig> connectedNodes = new ArrayList<>();
private final List<NodeConfig> searchNodes = new ArrayList<>();

private NodeConfig(NodeType type, @Nullable String name) {
this.type = type;
this.name = name;
this.address = getNonLoopbackIpv4Address();
this.hzPort = NetworkUtils.getNextAvailablePort(this.address);
this.connectedNodes.add(this);
switch (type) {
case SEARCH:
this.searchPort = NetworkUtils.getNextAvailablePort(this.address);
this.webPort = null;
this.searchNodes.add(this);
break;
case APPLICATION:
this.searchPort = null;
this.webPort = NetworkUtils.getNextAvailablePort(this.address);
break;
default:
throw new IllegalArgumentException();
}
}

NodeType getType() {
return type;
}

Optional<String> getName() {
return Optional.ofNullable(name);
}

InetAddress getAddress() {
return address;
}

int getHzPort() {
return hzPort;
}

Optional<Integer> getSearchPort() {
return Optional.ofNullable(searchPort);
}

Optional<Integer> getWebPort() {
return Optional.ofNullable(webPort);
}

String getHzHost() {
return address.getHostAddress() + ":" + hzPort;
}

String getSearchHost() {
return address.getHostAddress() + ":" + searchPort;
}

NodeConfig addConnectionToBus(NodeConfig... configs) {
connectedNodes.addAll(Arrays.asList(configs));
return this;
}

NodeConfig addConnectionToSearch(NodeConfig... configs) {
Arrays.stream(configs).forEach(config -> {
checkArgument(config.getType() == NodeType.SEARCH);
searchNodes.add(config);
});
return this;
}

List<NodeConfig> getConnectedNodes() {
return connectedNodes;
}

List<NodeConfig> getSearchNodes() {
return searchNodes;
}

static NodeConfig newApplicationConfig(String name) {
return new NodeConfig(NodeType.APPLICATION, name);
}

static NodeConfig newSearchConfig(String name) {
return new NodeConfig(NodeType.SEARCH, name);
}

/**
* See property sonar.cluster.hosts
*/
static void interconnectBus(NodeConfig... configs) {
Arrays.stream(configs).forEach(config -> Arrays.stream(configs).filter(c -> c != config).forEach(config::addConnectionToBus));
}

/**
* See property sonar.cluster.search.hosts
*/
static void interconnectSearch(NodeConfig... configs) {
Arrays.stream(configs).forEach(config -> Arrays.stream(configs)
.filter(c -> c.getType() == NodeType.SEARCH)
.forEach(config::addConnectionToSearch));
}

private static InetAddress getNonLoopbackIpv4Address() {
try {
Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
for (NetworkInterface networkInterface : Collections.list(nets)) {
if (!networkInterface.isLoopback() && networkInterface.isUp() && !isBlackListed(networkInterface)) {
Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
while (inetAddresses.hasMoreElements()) {
InetAddress inetAddress = inetAddresses.nextElement();
if (inetAddress instanceof Inet4Address) {
return inetAddress;
}
}
}
}
} catch (SocketException se) {
throw new RuntimeException("Cannot find a non loopback card required for tests", se);
}
throw new RuntimeException("Cannot find a non loopback card required for tests");
}

private static boolean isBlackListed(NetworkInterface networkInterface) {
return networkInterface.getName().startsWith("docker") ||
networkInterface.getName().startsWith("vboxnet");
}
}

Loading…
Cancel
Save