@@ -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, | |||
@@ -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() { |
@@ -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(); |
@@ -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(); | |||
} | |||
} |
@@ -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; |
@@ -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( |
@@ -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) { | |||
} | |||
} | |||
} |
@@ -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, |
@@ -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; | |||
} | |||
} |
@@ -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"); | |||
}; | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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"); | |||
} | |||
} |