Browse Source

SONAR-9741 web process shares health info in Hazelcast

tags/6.6-RC1
Sébastien Lesaint 6 years ago
parent
commit
17dc4c8315

+ 61
- 0
server/sonar-cluster/src/main/java/org/sonar/cluster/health/HealthStateRefresher.java View File

@@ -0,0 +1,61 @@
/*
* 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.cluster.health;

import java.util.concurrent.TimeUnit;
import org.picocontainer.Startable;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;

public class HealthStateRefresher implements Startable {
private static final Logger LOG = Loggers.get(HealthStateRefresher.class);
private static final int INITIAL_DELAY = 1;
private static final int DELAY = 10;

private final HealthStateRefresherExecutorService executorService;
private final NodeHealthProvider nodeHealthProvider;
private final SharedHealthState sharedHealthState;

public HealthStateRefresher(HealthStateRefresherExecutorService executorService, NodeHealthProvider nodeHealthProvider,
SharedHealthState sharedHealthState) {
this.executorService = executorService;
this.nodeHealthProvider = nodeHealthProvider;
this.sharedHealthState = sharedHealthState;
}

@Override
public void start() {
executorService.scheduleWithFixedDelay(this::refresh, INITIAL_DELAY, DELAY, TimeUnit.SECONDS);
}

private void refresh() {
try {
NodeHealth nodeHealth = nodeHealthProvider.get();
sharedHealthState.writeMine(nodeHealth);
} catch (Throwable t) {
LOG.error("An error occurred while attempting to refresh HealthState of the current node in the shared state:", t);
}
}

@Override
public void stop() {
// nothing to do
}
}

+ 25
- 0
server/sonar-cluster/src/main/java/org/sonar/cluster/health/HealthStateRefresherExecutorService.java View File

@@ -0,0 +1,25 @@
/*
* 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.cluster.health;

import java.util.concurrent.ScheduledExecutorService;

public interface HealthStateRefresherExecutorService extends ScheduledExecutorService {
}

+ 97
- 0
server/sonar-cluster/src/test/java/org/sonar/cluster/health/HealthStateRefresherTest.java View File

@@ -0,0 +1,97 @@
/*
* 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.cluster.health;

import java.util.Random;
import java.util.concurrent.TimeUnit;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.ArgumentCaptor;
import org.sonar.api.utils.log.LogTester;
import org.sonar.api.utils.log.LoggerLevel;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;

public class HealthStateRefresherTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Rule
public LogTester logTester = new LogTester();

private Random random = new Random();
private NodeDetailsTestSupport testSupport = new NodeDetailsTestSupport(random);

private HealthStateRefresherExecutorService executorService = mock(HealthStateRefresherExecutorService.class);
private NodeHealthProvider nodeHealthProvider = mock(NodeHealthProvider.class);
private SharedHealthState sharedHealthState = mock(SharedHealthState.class);
private HealthStateRefresher underTest = new HealthStateRefresher(executorService, nodeHealthProvider, sharedHealthState);

@Test
public void start_adds_runnable_with_10_second_delay_and_initial_delay_putting_NodeHealth_from_provider_into_SharedHealthState() {
ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
NodeHealth[] nodeHealths = {
testSupport.randomNodeHealth(),
testSupport.randomNodeHealth(),
testSupport.randomNodeHealth()
};
Error expected = new Error("Simulating exception raised by NodeHealthProvider");
when(nodeHealthProvider.get())
.thenReturn(nodeHealths[0])
.thenReturn(nodeHealths[1])
.thenReturn(nodeHealths[2])
.thenThrow(expected);

underTest.start();

verify(executorService).scheduleWithFixedDelay(runnableCaptor.capture(), eq(1L), eq(10L), eq(TimeUnit.SECONDS));

Runnable runnable = runnableCaptor.getValue();
runnable.run();
runnable.run();
runnable.run();

verify(sharedHealthState).writeMine(nodeHealths[0]);
verify(sharedHealthState).writeMine(nodeHealths[1]);
verify(sharedHealthState).writeMine(nodeHealths[2]);

try {
runnable.run();
assertThat(logTester.logs()).hasSize(1);
assertThat(logTester.logs(LoggerLevel.ERROR).iterator().next())
.isEqualTo("An error occurred while attempting to refresh HealthState of the current node in the shared state:");
} catch (IllegalStateException e) {
fail("Runnable should catch any Throwable");
}
}

@Test
public void stop_has_no_effect() {
underTest.stop();

verifyZeroInteractions(executorService, nodeHealthProvider, sharedHealthState);
}
}

+ 6
- 29
server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeDetailsTest.java View File

@@ -20,10 +20,8 @@
package org.sonar.cluster.health;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Random;
import org.junit.Rule;
import org.junit.Test;
@@ -38,7 +36,8 @@ public class NodeDetailsTest {
public ExpectedException expectedException = ExpectedException.none();

private Random random = new Random();
private NodeDetails.Type randomType = NodeDetails.Type.values()[random.nextInt(NodeDetails.Type.values().length)];
private NodeDetailsTestSupport testSupport = new NodeDetailsTestSupport(random);
private NodeDetails.Type randomType = testSupport.randomType();
private NodeDetails.Builder builderUnderTest = newNodeDetailsBuilder();

@Test
@@ -173,7 +172,7 @@ public class NodeDetailsTest {

@Test
public void equals_is_based_on_content() {
NodeDetails.Builder builder = randomBuilder();
NodeDetails.Builder builder = testSupport.randomNodeDetailsBuilder();

NodeDetails underTest = builder.build();

@@ -187,7 +186,7 @@ public class NodeDetailsTest {

@Test
public void hashcode_is_based_on_content() {
NodeDetails.Builder builder = randomBuilder();
NodeDetails.Builder builder = testSupport.randomNodeDetailsBuilder();

NodeDetails underTest = builder.build();

@@ -197,8 +196,8 @@ public class NodeDetailsTest {

@Test
public void NodeDetails_is_Externalizable() throws IOException, ClassNotFoundException {
NodeDetails source = randomNodeDetails();
byte[] byteArray = serialize(source);
NodeDetails source = testSupport.randomNodeDetails();
byte[] byteArray = testSupport.serialize(source);

NodeDetails underTest = (NodeDetails) new ObjectInputStream(new ByteArrayInputStream(byteArray)).readObject();

@@ -245,26 +244,4 @@ public class NodeDetailsTest {
assertThat(underTest.getPort()).isEqualTo(port);
assertThat(underTest.getStarted()).isEqualTo(started);
}

private NodeDetails randomNodeDetails() {
return randomBuilder()
.build();
}

private NodeDetails.Builder randomBuilder() {
return newNodeDetailsBuilder()
.setType(randomType)
.setName(randomAlphanumeric(3))
.setHost(randomAlphanumeric(10))
.setPort(1 + random.nextInt(10))
.setStarted(1 + random.nextInt(666));
}

private static byte[] serialize(NodeDetails source) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(out)) {
objectOutputStream.writeObject(source);
}
return out.toByteArray();
}
}

+ 89
- 0
server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeDetailsTestSupport.java View File

@@ -0,0 +1,89 @@
/*
* 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.cluster.health;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Random;
import java.util.stream.IntStream;

import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.sonar.cluster.health.NodeDetails.newNodeDetailsBuilder;
import static org.sonar.cluster.health.NodeHealth.newNodeHealthBuilder;

public class NodeDetailsTestSupport {
private final Random random;

public NodeDetailsTestSupport() {
this(new Random());
}

NodeDetailsTestSupport(Random random) {
this.random = random;
}

NodeHealth.Status randomStatus() {
return NodeHealth.Status.values()[random.nextInt(NodeHealth.Status.values().length)];
}

NodeHealth randomNodeHealth() {
return randomBuilder().build();
}

NodeHealth.Builder randomBuilder() {
return randomBuilder(0);
}

NodeHealth.Builder randomBuilder(int minCauseCount) {
NodeHealth.Builder builder = newNodeHealthBuilder()
.setStatus(randomStatus())
.setDetails(randomNodeDetails())
.setDate(1 + random.nextInt(33));
IntStream.range(0, minCauseCount + random.nextInt(2)).mapToObj(i -> randomAlphanumeric(4)).forEach(builder::addCause);
return builder;
}

NodeDetails randomNodeDetails() {
return randomNodeDetailsBuilder()
.build();
}

NodeDetails.Builder randomNodeDetailsBuilder() {
return newNodeDetailsBuilder()
.setType(randomType())
.setName(randomAlphanumeric(3))
.setHost(randomAlphanumeric(10))
.setPort(1 + random.nextInt(10))
.setStarted(1 + random.nextInt(666));
}

NodeDetails.Type randomType() {
return NodeDetails.Type.values()[random.nextInt(NodeDetails.Type.values().length)];
}

static byte[] serialize(Object source) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(out)) {
objectOutputStream.writeObject(source);
}
return out.toByteArray();
}
}

+ 14
- 52
server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeHealthTest.java View File

@@ -20,10 +20,8 @@
package org.sonar.cluster.health;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Arrays;
import java.util.Random;
import java.util.stream.IntStream;
@@ -33,8 +31,6 @@ import org.junit.rules.ExpectedException;

import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.cluster.health.NodeDetails.Type;
import static org.sonar.cluster.health.NodeDetails.newNodeDetailsBuilder;
import static org.sonar.cluster.health.NodeHealth.newNodeHealthBuilder;

public class NodeHealthTest {
@@ -42,7 +38,8 @@ public class NodeHealthTest {
public ExpectedException expectedException = ExpectedException.none();

private Random random = new Random();
private NodeHealth.Status randomStatus = NodeHealth.Status.values()[random.nextInt(NodeHealth.Status.values().length)];
private NodeDetailsTestSupport testSupport = new NodeDetailsTestSupport(random);
private NodeHealth.Status randomStatus = testSupport.randomStatus();
private NodeHealth.Builder builderUnderTest = newNodeHealthBuilder();

@Test
@@ -91,7 +88,7 @@ public class NodeHealthTest {
public void build_throws_IAE_if_date_is_less_than_1() {
builderUnderTest
.setStatus(randomStatus)
.setDetails(randomNodeDetails());
.setDetails(testSupport.randomNodeDetails());

expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("date must be > 0");
@@ -101,7 +98,7 @@ public class NodeHealthTest {

@Test
public void clearClauses_clears_clauses_of_builder() {
NodeHealth.Builder underTest = randomBuilder();
NodeHealth.Builder underTest = testSupport.randomBuilder();
NodeHealth original = underTest
.addCause(randomAlphanumeric(3))
.build();
@@ -117,12 +114,12 @@ public class NodeHealthTest {

@Test
public void builder_can_be_reused() {
NodeHealth.Builder builder = randomBuilder(1);
NodeHealth.Builder builder = testSupport.randomBuilder(1);
NodeHealth original = builder.build();
NodeHealth second = builder.build();

NodeHealth.Status newRandomStatus = NodeHealth.Status.values()[random.nextInt(NodeHealth.Status.values().length)];
NodeDetails newNodeDetails = randomNodeDetails();
NodeDetails newNodeDetails = testSupport.randomNodeDetails();
long newDate = 1 + random.nextInt(666);
builder
.clearCauses()
@@ -143,7 +140,7 @@ public class NodeHealthTest {

@Test
public void equals_is_based_on_content() {
NodeHealth.Builder builder = randomBuilder();
NodeHealth.Builder builder = testSupport.randomBuilder();

NodeHealth underTest = builder.build();

@@ -157,7 +154,7 @@ public class NodeHealthTest {

@Test
public void hashcode_is_based_on_content() {
NodeHealth.Builder builder = randomBuilder();
NodeHealth.Builder builder = testSupport.randomBuilder();

NodeHealth underTest = builder.build();

@@ -167,8 +164,8 @@ public class NodeHealthTest {

@Test
public void class_is_serializable_with_causes() throws IOException, ClassNotFoundException {
NodeHealth source = randomBuilder(1).build();
byte[] bytes = serialize(source);
NodeHealth source = testSupport.randomBuilder(1).build();
byte[] bytes = testSupport.serialize(source);

NodeHealth underTest = (NodeHealth) new ObjectInputStream(new ByteArrayInputStream(bytes)).readObject();

@@ -179,27 +176,19 @@ public class NodeHealthTest {
public void class_is_serializable_without_causes() throws IOException, ClassNotFoundException {
NodeHealth.Builder builder = newNodeHealthBuilder()
.setStatus(randomStatus)
.setDetails(randomNodeDetails())
.setDetails(testSupport.randomNodeDetails())
.setDate(1 + random.nextInt(999));
NodeHealth source = builder.build();
byte[] bytes = serialize(source);
byte[] bytes = testSupport.serialize(source);

NodeHealth underTest = (NodeHealth) new ObjectInputStream(new ByteArrayInputStream(bytes)).readObject();

assertThat(underTest).isEqualTo(source);
}

private byte[] serialize(NodeHealth source) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(out)) {
objectOutputStream.writeObject(source);
}
return out.toByteArray();
}

@Test
public void verify_toString() {
NodeDetails nodeDetails = randomNodeDetails();
NodeDetails nodeDetails = testSupport.randomNodeDetails();
int date = 1 + random.nextInt(999);
String cause = randomAlphanumeric(4);
NodeHealth.Builder builder = builderUnderTest
@@ -216,7 +205,7 @@ public class NodeHealthTest {

@Test
public void verify_getters() {
NodeDetails nodeDetails = randomNodeDetails();
NodeDetails nodeDetails = testSupport.randomNodeDetails();
int date = 1 + random.nextInt(999);
NodeHealth.Builder builder = builderUnderTest
.setStatus(randomStatus)
@@ -233,31 +222,4 @@ public class NodeHealthTest {
assertThat(underTest.getCauses()).containsOnly(causes);
}

private NodeHealth.Builder randomBuilder() {
return randomBuilder(0);
}

private NodeHealth.Builder randomBuilder(int minCauseCount) {
NodeHealth.Builder builder = newNodeHealthBuilder()
.setStatus(randomStatus)
.setDetails(randomNodeDetails())
.setDate(1 + random.nextInt(33));
IntStream.range(0, minCauseCount + random.nextInt(2)).mapToObj(i -> randomAlphanumeric(4)).forEach(builder::addCause);
return builder;
}

private NodeDetails randomNodeDetails() {
return randomNodeDetailsBuilder()
.build();
}

private NodeDetails.Builder randomNodeDetailsBuilder() {
return newNodeDetailsBuilder()
.setType(Type.values()[random.nextInt(Type.values().length)])
.setName(randomAlphanumeric(3))
.setHost(randomAlphanumeric(10))
.setPort(1 + random.nextInt(10))
.setStarted(1 + random.nextInt(666));
}

}

+ 22
- 11
server/sonar-server/pom.xml View File

@@ -85,8 +85,14 @@
</exclusions>
</dependency>
<dependency>
<groupId>org.sonarsource.update-center</groupId>
<artifactId>sonar-update-center-common</artifactId>
<groupId>${project.groupId}</groupId>
<artifactId>sonar-process</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>sonar-cluster</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
@@ -112,6 +118,20 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>sonar-ws</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>sonar-plugin-bridge</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.sonarsource.update-center</groupId>
<artifactId>sonar-update-center-common</artifactId>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
@@ -175,20 +195,11 @@
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>sonar-ws</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>sonar-plugin-bridge</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>

+ 38
- 0
server/sonar-server/src/main/java/org/sonar/server/health/HealthStateRefresherExecutorServiceImpl.java View File

@@ -0,0 +1,38 @@
/*
* 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.health;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import org.sonar.cluster.health.HealthStateRefresherExecutorService;
import org.sonar.server.util.AbstractStoppableScheduledExecutorServiceImpl;

public class HealthStateRefresherExecutorServiceImpl
extends AbstractStoppableScheduledExecutorServiceImpl<ScheduledExecutorService>
implements HealthStateRefresherExecutorService {
public HealthStateRefresherExecutorServiceImpl() {
super(Executors.newSingleThreadScheduledExecutor(
new ThreadFactoryBuilder()
.setDaemon(false)
.setNameFormat("health_state_refresh-%d")
.build()));
}
}

+ 35
- 0
server/sonar-server/src/main/java/org/sonar/server/health/NodeHealthModule.java View File

@@ -0,0 +1,35 @@
/*
* 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.health;

import org.sonar.cluster.health.HealthStateRefresher;
import org.sonar.cluster.health.SharedHealthStateImpl;
import org.sonar.core.platform.Module;

public class NodeHealthModule extends Module {
@Override
protected void configureModule() {
add(
NodeHealthProviderImpl.class,
HealthStateRefresherExecutorServiceImpl.class,
HealthStateRefresher.class,
SharedHealthStateImpl.class);
}
}

+ 88
- 0
server/sonar-server/src/main/java/org/sonar/server/health/NodeHealthProviderImpl.java View File

@@ -0,0 +1,88 @@
/*
* 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.health;

import java.util.Date;
import java.util.function.Supplier;
import org.sonar.NetworkUtils;
import org.sonar.api.config.Configuration;
import org.sonar.api.platform.Server;
import org.sonar.cluster.health.NodeDetails;
import org.sonar.cluster.health.NodeHealth;
import org.sonar.cluster.health.NodeHealthProvider;

import static java.lang.String.format;
import static org.sonar.cluster.ClusterProperties.CLUSTER_NODE_HOST;
import static org.sonar.cluster.ClusterProperties.CLUSTER_NODE_NAME;
import static org.sonar.cluster.ClusterProperties.CLUSTER_NODE_PORT;
import static org.sonar.cluster.health.NodeDetails.newNodeDetailsBuilder;
import static org.sonar.cluster.health.NodeHealth.newNodeHealthBuilder;

public class NodeHealthProviderImpl implements NodeHealthProvider {
private final HealthChecker healthChecker;
private final NodeHealth.Builder nodeHealthBuilder;
private final NodeDetails nodeDetails;

public NodeHealthProviderImpl(Configuration configuration, HealthChecker healthChecker, Server server, NetworkUtils networkUtils) {
this.healthChecker = healthChecker;
this.nodeHealthBuilder = newNodeHealthBuilder();
this.nodeDetails = newNodeDetailsBuilder()
.setName(computeName(configuration))
.setType(NodeDetails.Type.APPLICATION)
.setHost(computeHost(configuration, networkUtils))
.setPort(computePort(configuration))
.setStarted(server.getStartedAt().getTime())
.build();
}

private static String computeName(Configuration configuration) {
return configuration.get(CLUSTER_NODE_NAME)
.orElseThrow(missingPropertyISE(CLUSTER_NODE_NAME));
}

private static String computeHost(Configuration configuration, NetworkUtils networkUtils) {
return configuration.get(CLUSTER_NODE_HOST)
.filter(s -> !s.isEmpty())
.orElseGet(networkUtils::getHostname);
}

private static int computePort(Configuration configuration) {
return configuration.getInt(CLUSTER_NODE_PORT)
.orElseThrow(missingPropertyISE(CLUSTER_NODE_PORT));
}

private static Supplier<IllegalStateException> missingPropertyISE(String propertyName) {
return () -> new IllegalStateException(format("Property %s is not defined", propertyName));
}

@Override
public NodeHealth get() {
Health nodeHealth = healthChecker.checkNode();
this.nodeHealthBuilder
.clearCauses()
.setStatus(NodeHealth.Status.valueOf(nodeHealth.getStatus().name()));
nodeHealth.getCauses().forEach(this.nodeHealthBuilder::addCause);

return this.nodeHealthBuilder
.setDate(new Date().getTime())
.setDetails(nodeDetails)
.build();
}
}

+ 3
- 1
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java View File

@@ -21,7 +21,7 @@ package org.sonar.server.platform.platformlevel;

import java.util.Properties;
import javax.annotation.Nullable;
import org.sonar.db.DBSessionsImpl;
import org.sonar.NetworkUtils;
import org.sonar.api.SonarQubeSide;
import org.sonar.api.SonarQubeVersion;
import org.sonar.api.internal.ApiVersion;
@@ -32,6 +32,7 @@ import org.sonar.api.utils.internal.TempFolderCleaner;
import org.sonar.core.config.ConfigurationProvider;
import org.sonar.core.config.CorePropertyDefinitions;
import org.sonar.core.util.UuidFactoryImpl;
import org.sonar.db.DBSessionsImpl;
import org.sonar.db.DaoModule;
import org.sonar.db.DatabaseChecker;
import org.sonar.db.DbClient;
@@ -84,6 +85,7 @@ public class PlatformLevel1 extends PlatformLevel {
ProcessCommandWrapperImpl.class,
RestartFlagHolderImpl.class,
UuidFactoryImpl.INSTANCE,
NetworkUtils.INSTANCE,
UrlSettings.class,
EmbeddedDatabaseFactory.class,
LogbackHelper.class,

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

@@ -31,6 +31,7 @@ import org.sonar.api.rules.XMLRuleParser;
import org.sonar.api.server.rule.RulesDefinitionXmlLoader;
import org.sonar.ce.CeModule;
import org.sonar.ce.settings.ProjectConfigurationFactory;
import org.sonar.cluster.localclient.HazelcastLocalClient;
import org.sonar.core.component.DefaultResourceTypes;
import org.sonar.core.timemachine.Periods;
import org.sonar.server.authentication.AuthenticationModule;
@@ -61,6 +62,7 @@ import org.sonar.server.es.metadata.MetadataIndex;
import org.sonar.server.es.metadata.MetadataIndexDefinition;
import org.sonar.server.event.NewAlerts;
import org.sonar.server.favorite.FavoriteModule;
import org.sonar.server.health.NodeHealthModule;
import org.sonar.server.issue.AddTagsAction;
import org.sonar.server.issue.AssignAction;
import org.sonar.server.issue.CommentAction;
@@ -243,6 +245,10 @@ public class PlatformLevel4 extends PlatformLevel {
MetadataIndex.class,
EsDbCompatibilityImpl.class);

addIfCluster(
HazelcastLocalClient.class,
NodeHealthModule.class);

add(
PluginDownloader.class,
DeprecatedViews.class,

+ 5
- 6
server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthActionModule.java View File

@@ -27,17 +27,16 @@ import org.sonar.server.health.HealthCheckerImpl;
import org.sonar.server.health.WebServerStatusNodeCheck;

public class HealthActionModule extends Module {

@Override
protected void configureModule() {
add(
// NodeHealthCheck implementations
WebServerStatusNodeCheck.class,
// NodeHealthCheck implementations
add(WebServerStatusNodeCheck.class,
DbConnectionNodeCheck.class,
EsStatusNodeCheck.class,
CeStatusNodeCheck.class,

HealthCheckerImpl.class,
CeStatusNodeCheck.class);

add(HealthCheckerImpl.class,
HealthAction.class);
}
}

+ 87
- 0
server/sonar-server/src/test/java/org/sonar/server/health/NodeHealthModuleTest.java View File

@@ -0,0 +1,87 @@
/*
* 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.health;

import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import org.junit.Test;
import org.picocontainer.ComponentAdapter;
import org.sonar.NetworkUtils;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.platform.Server;
import org.sonar.api.utils.System2;
import org.sonar.cluster.health.SharedHealthStateImpl;
import org.sonar.cluster.localclient.HazelcastClient;
import org.sonar.core.platform.ComponentContainer;

import static java.lang.String.valueOf;
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class NodeHealthModuleTest {
private Random random = new Random();
private MapSettings mapSettings = new MapSettings();
private NodeHealthModule underTest = new NodeHealthModule();

@Test
public void no_broken_dependencies() {
ComponentContainer container = new ComponentContainer();
Server server = mock(Server.class);
NetworkUtils networkUtils = mock(NetworkUtils.class);
// settings required by NodeHealthProvider
mapSettings.setProperty("sonar.cluster.node.name", randomAlphanumeric(3));
mapSettings.setProperty("sonar.cluster.node.port", valueOf(1 + random.nextInt(10)));
when(server.getStartedAt()).thenReturn(new Date());
when(networkUtils.getHostname()).thenReturn(randomAlphanumeric(12));
// upper level dependencies
container.add(
mock(System2.class),
mapSettings.asConfig(),
server,
networkUtils,
mock(HazelcastClient.class));
// HealthAction dependencies
container.add(mock(HealthChecker.class));

underTest.configure(container);

container.startComponents();
}

@Test
public void provides_implementation_of_SharedHealthState() {
ComponentContainer container = new ComponentContainer();

underTest.configure(container);

assertThat(classesAddedToContainer(container))
.contains(SharedHealthStateImpl.class);
}

private List<Class<?>> classesAddedToContainer(ComponentContainer container) {
Collection<ComponentAdapter<?>> componentAdapters = container.getPicoContainer().getComponentAdapters();
return componentAdapters.stream().map(ComponentAdapter::getComponentImplementation).collect(Collectors.toList());
}
}

+ 242
- 0
server/sonar-server/src/test/java/org/sonar/server/health/NodeHealthProviderImplTest.java View File

@@ -0,0 +1,242 @@
/*
* 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.health;

import java.util.Arrays;
import java.util.Date;
import java.util.Random;
import java.util.stream.IntStream;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.NetworkUtils;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.platform.Server;
import org.sonar.cluster.health.NodeDetails;
import org.sonar.cluster.health.NodeHealth;

import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.sonar.cluster.ClusterProperties.CLUSTER_NODE_HOST;
import static org.sonar.cluster.ClusterProperties.CLUSTER_NODE_NAME;
import static org.sonar.cluster.ClusterProperties.CLUSTER_NODE_PORT;

public class NodeHealthProviderImplTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();

private final Random random = new Random();
private MapSettings mapSettings = new MapSettings();
private HealthChecker healthChecker = mock(HealthChecker.class);
private Server server = mock(Server.class);
private NetworkUtils networkUtils = mock(NetworkUtils.class);

@Test
public void constructor_throws_ISE_if_node_name_property_is_not_set() {
expectedException.expect(IllegalStateException.class);
expectedException.expectMessage("Property sonar.cluster.node.name is not defined");

new NodeHealthProviderImpl(mapSettings.asConfig(), healthChecker, server, networkUtils);
}

@Test
public void constructor_thows_NPE_if_NetworkUtils_getHostname_returns_null() {
mapSettings.setProperty(CLUSTER_NODE_NAME, randomAlphanumeric(3));

expectedException.expect(NullPointerException.class);

new NodeHealthProviderImpl(mapSettings.asConfig(), healthChecker, server, networkUtils);
}

@Test
public void constructor_throws_ISE_if_node_port_property_is_not_set() {
mapSettings.setProperty(CLUSTER_NODE_NAME, randomAlphanumeric(3));
when(networkUtils.getHostname()).thenReturn(randomAlphanumeric(23));

expectedException.expect(IllegalStateException.class);
expectedException.expectMessage("Property sonar.cluster.node.port is not defined");

new NodeHealthProviderImpl(mapSettings.asConfig(), healthChecker, server, networkUtils);
}

@Test
public void constructor_throws_NPE_is_Server_getStartedAt_is_null() {
setRequiredPropertiesForConstructor();

expectedException.expect(NullPointerException.class);

new NodeHealthProviderImpl(mapSettings.asConfig(), healthChecker, server, networkUtils);
}

@Test
public void get_returns_HEALTH_status_and_causes_from_HealthChecker_checkNode() {
setRequiredPropertiesForConstructor();
setStartedAt();
when(networkUtils.getHostname()).thenReturn(randomAlphanumeric(4));
Health.Status randomStatus = Health.Status.values()[random.nextInt(Health.Status.values().length)];
String[] expected = IntStream.range(0, random.nextInt(4)).mapToObj(s -> randomAlphabetic(55)).toArray(String[]::new);
Health.Builder healthBuilder = Health.newHealthCheckBuilder()
.setStatus(randomStatus);
Arrays.stream(expected).forEach(healthBuilder::addCause);
when(healthChecker.checkNode()).thenReturn(healthBuilder.build());
NodeHealthProviderImpl underTest = new NodeHealthProviderImpl(mapSettings.asConfig(), healthChecker, server, networkUtils);

NodeHealth nodeHealth = underTest.get();

assertThat(nodeHealth.getStatus().name()).isEqualTo(randomStatus.name());
assertThat(nodeHealth.getCauses()).containsOnly(expected);
}

@Test
public void get_returns_APPLICATION_type() {
setRequiredPropertiesForConstructor();
setStartedAt();
when(networkUtils.getHostname()).thenReturn(randomAlphanumeric(23));
when(healthChecker.checkNode()).thenReturn(Health.newHealthCheckBuilder()
.setStatus(Health.Status.values()[random.nextInt(Health.Status.values().length)])
.build());
NodeHealthProviderImpl underTest = new NodeHealthProviderImpl(mapSettings.asConfig(), healthChecker, server, networkUtils);

NodeHealth nodeHealth = underTest.get();

assertThat(nodeHealth.getDetails().getType()).isEqualTo(NodeDetails.Type.APPLICATION);
}

@Test
public void get_returns_name_and_port_from_properties_at_constructor_time() {
String name = randomAlphanumeric(3);
int port = 1 + random.nextInt(4);
mapSettings.setProperty(CLUSTER_NODE_NAME, name);
mapSettings.setProperty(CLUSTER_NODE_PORT, port);
setStartedAt();
when(healthChecker.checkNode()).thenReturn(Health.newHealthCheckBuilder()
.setStatus(Health.Status.values()[random.nextInt(Health.Status.values().length)])
.build());
when(networkUtils.getHostname()).thenReturn(randomAlphanumeric(3));
NodeHealthProviderImpl underTest = new NodeHealthProviderImpl(mapSettings.asConfig(), healthChecker, server, networkUtils);

NodeHealth nodeHealth = underTest.get();

assertThat(nodeHealth.getDetails().getName()).isEqualTo(name);
assertThat(nodeHealth.getDetails().getPort()).isEqualTo(port);

// change values in properties
setRequiredPropertiesForConstructor();

NodeHealth newNodeHealth = underTest.get();

assertThat(newNodeHealth.getDetails().getName()).isEqualTo(name);
assertThat(newNodeHealth.getDetails().getPort()).isEqualTo(port);
}

@Test
public void get_returns_host_from_property_if_set_at_constructor_time() {
String host = randomAlphanumeric(4);
mapSettings.setProperty(CLUSTER_NODE_NAME, randomAlphanumeric(3));
mapSettings.setProperty(CLUSTER_NODE_PORT, 1 + random.nextInt(4));
mapSettings.setProperty(CLUSTER_NODE_HOST, host);
setStartedAt();
when(healthChecker.checkNode()).thenReturn(Health.newHealthCheckBuilder()
.setStatus(Health.Status.values()[random.nextInt(Health.Status.values().length)])
.build());
NodeHealthProviderImpl underTest = new NodeHealthProviderImpl(mapSettings.asConfig(), healthChecker, server, networkUtils);

NodeHealth nodeHealth = underTest.get();

assertThat(nodeHealth.getDetails().getHost()).isEqualTo(host);

// change values in properties
mapSettings.setProperty(CLUSTER_NODE_HOST, randomAlphanumeric(66));

NodeHealth newNodeHealth = underTest.get();

assertThat(newNodeHealth.getDetails().getHost()).isEqualTo(host);
}

@Test
public void get_returns_hostname_from_NetworkUtils_if_property_is_not_set_at_constructor_time() {
getReturnsHostnameFromNetworkUtils(null);
}

@Test
public void get_returns_hostname_from_NetworkUtils_if_property_is_empty_at_constructor_time() {
getReturnsHostnameFromNetworkUtils(random.nextBoolean() ? "" : " ");
}

private void getReturnsHostnameFromNetworkUtils(String hostPropertyValue) {
String host = randomAlphanumeric(3);
setRequiredPropertiesForConstructor();
if (hostPropertyValue != null) {
mapSettings.setProperty(CLUSTER_NODE_HOST, hostPropertyValue);
}
setStartedAt();
when(healthChecker.checkNode()).thenReturn(Health.newHealthCheckBuilder()
.setStatus(Health.Status.values()[random.nextInt(Health.Status.values().length)])
.build());
when(networkUtils.getHostname()).thenReturn(host);
NodeHealthProviderImpl underTest = new NodeHealthProviderImpl(mapSettings.asConfig(), healthChecker, server, networkUtils);

NodeHealth nodeHealth = underTest.get();

assertThat(nodeHealth.getDetails().getHost()).isEqualTo(host);

// change hostname
when(networkUtils.getHostname()).thenReturn(randomAlphanumeric(4));

NodeHealth newNodeHealth = underTest.get();

assertThat(newNodeHealth.getDetails().getHost()).isEqualTo(host);
}

@Test
public void get_returns_started_from_server_startedAt_at_constructor_time() {
setRequiredPropertiesForConstructor();
when(networkUtils.getHostname()).thenReturn(randomAlphanumeric(4));
Date date = new Date();
when(server.getStartedAt()).thenReturn(date);
when(healthChecker.checkNode()).thenReturn(Health.newHealthCheckBuilder()
.setStatus(Health.Status.values()[random.nextInt(Health.Status.values().length)])
.build());
NodeHealthProviderImpl underTest = new NodeHealthProviderImpl(mapSettings.asConfig(), healthChecker, server, networkUtils);

NodeHealth nodeHealth = underTest.get();

assertThat(nodeHealth.getDetails().getStarted()).isEqualTo(date.getTime());

// change startedAt value
setStartedAt();

NodeHealth newNodeHealth = underTest.get();

assertThat(newNodeHealth.getDetails().getStarted()).isEqualTo(date.getTime());
}

private void setStartedAt() {
when(server.getStartedAt()).thenReturn(new Date());
}

private void setRequiredPropertiesForConstructor() {
mapSettings.setProperty(CLUSTER_NODE_NAME, randomAlphanumeric(3));
mapSettings.setProperty(CLUSTER_NODE_PORT, 1 + random.nextInt(4));
}
}

Loading…
Cancel
Save