diff options
author | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2017-08-30 10:53:17 +0200 |
---|---|---|
committer | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2017-09-13 15:50:51 +0200 |
commit | 17dc4c8315d78a346df6e2b93313d6bc72f14992 (patch) | |
tree | 503b96d296e2859ccd1f7a24aac8374c6340c0c8 /server | |
parent | 0d06d27d3c0f1a35384b40926e34dc0020de998d (diff) | |
download | sonarqube-17dc4c8315d78a346df6e2b93313d6bc72f14992.tar.gz sonarqube-17dc4c8315d78a346df6e2b93313d6bc72f14992.zip |
SONAR-9741 web process shares health info in Hazelcast
Diffstat (limited to 'server')
15 files changed, 818 insertions, 99 deletions
diff --git a/server/sonar-cluster/src/main/java/org/sonar/cluster/health/HealthStateRefresher.java b/server/sonar-cluster/src/main/java/org/sonar/cluster/health/HealthStateRefresher.java new file mode 100644 index 00000000000..d37989dfa2d --- /dev/null +++ b/server/sonar-cluster/src/main/java/org/sonar/cluster/health/HealthStateRefresher.java @@ -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 + } +} diff --git a/server/sonar-cluster/src/main/java/org/sonar/cluster/health/HealthStateRefresherExecutorService.java b/server/sonar-cluster/src/main/java/org/sonar/cluster/health/HealthStateRefresherExecutorService.java new file mode 100644 index 00000000000..5fd841ff591 --- /dev/null +++ b/server/sonar-cluster/src/main/java/org/sonar/cluster/health/HealthStateRefresherExecutorService.java @@ -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 { +} diff --git a/server/sonar-cluster/src/test/java/org/sonar/cluster/health/HealthStateRefresherTest.java b/server/sonar-cluster/src/test/java/org/sonar/cluster/health/HealthStateRefresherTest.java new file mode 100644 index 00000000000..edcc89af1fa --- /dev/null +++ b/server/sonar-cluster/src/test/java/org/sonar/cluster/health/HealthStateRefresherTest.java @@ -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); + } +} diff --git a/server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeDetailsTest.java b/server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeDetailsTest.java index 1a1947c9c94..6ce0be03349 100644 --- a/server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeDetailsTest.java +++ b/server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeDetailsTest.java @@ -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(); - } } diff --git a/server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeDetailsTestSupport.java b/server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeDetailsTestSupport.java new file mode 100644 index 00000000000..99b0011f6e9 --- /dev/null +++ b/server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeDetailsTestSupport.java @@ -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(); + } +} diff --git a/server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeHealthTest.java b/server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeHealthTest.java index f1a899646ea..0484fa6883d 100644 --- a/server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeHealthTest.java +++ b/server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeHealthTest.java @@ -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)); - } - } diff --git a/server/sonar-server/pom.xml b/server/sonar-server/pom.xml index 95dae9f72e7..747da2c4c96 100644 --- a/server/sonar-server/pom.xml +++ b/server/sonar-server/pom.xml @@ -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> @@ -113,6 +119,20 @@ </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> </dependency> @@ -176,20 +196,11 @@ <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> </dependency> diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/HealthStateRefresherExecutorServiceImpl.java b/server/sonar-server/src/main/java/org/sonar/server/health/HealthStateRefresherExecutorServiceImpl.java new file mode 100644 index 00000000000..472a6565ede --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/health/HealthStateRefresherExecutorServiceImpl.java @@ -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())); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/NodeHealthModule.java b/server/sonar-server/src/main/java/org/sonar/server/health/NodeHealthModule.java new file mode 100644 index 00000000000..c08daf0f934 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/health/NodeHealthModule.java @@ -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); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/NodeHealthProviderImpl.java b/server/sonar-server/src/main/java/org/sonar/server/health/NodeHealthProviderImpl.java new file mode 100644 index 00000000000..0c3b263c752 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/health/NodeHealthProviderImpl.java @@ -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(); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java index 3941da73054..7b7fc68f854 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java @@ -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, diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index bda21231f0a..f6c7a77d648 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -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, diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthActionModule.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthActionModule.java index 2dad8eaf37f..871a9314ee6 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthActionModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthActionModule.java @@ -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); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/health/NodeHealthModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/health/NodeHealthModuleTest.java new file mode 100644 index 00000000000..295d3c70eb9 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/health/NodeHealthModuleTest.java @@ -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()); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/health/NodeHealthProviderImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/health/NodeHealthProviderImplTest.java new file mode 100644 index 00000000000..02a0df617c8 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/health/NodeHealthProviderImplTest.java @@ -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)); + } +} |