aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>2017-09-05 16:08:54 +0200
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>2017-09-13 15:50:50 +0200
commit893b3d2f8d5e5d2789cd5c954d8016742bfc18e5 (patch)
tree3486fa7c5d8e1a5bbfdcfbc9b1c3212be0ae7eda /server
parent128f5f74e20b7391331840493a899f8bec494059 (diff)
downloadsonarqube-893b3d2f8d5e5d2789cd5c954d8016742bfc18e5.tar.gz
sonarqube-893b3d2f8d5e5d2789cd5c954d8016742bfc18e5.zip
SONAR-9741 add SharedHealthState
Diffstat (limited to 'server')
-rw-r--r--server/sonar-cluster/pom.xml8
-rw-r--r--server/sonar-cluster/src/main/java/org/sonar/cluster/health/NodeDetails.java200
-rw-r--r--server/sonar-cluster/src/main/java/org/sonar/cluster/health/NodeHealth.java189
-rw-r--r--server/sonar-cluster/src/main/java/org/sonar/cluster/health/NodeHealthProvider.java29
-rw-r--r--server/sonar-cluster/src/main/java/org/sonar/cluster/health/SharedHealthState.java34
-rw-r--r--server/sonar-cluster/src/main/java/org/sonar/cluster/health/SharedHealthStateImpl.java61
-rw-r--r--server/sonar-cluster/src/main/java/org/sonar/cluster/health/package-info.java23
-rw-r--r--server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeDetailsTest.java270
-rw-r--r--server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeHealthTest.java263
-rw-r--r--server/sonar-cluster/src/test/java/org/sonar/cluster/health/SharedHealthStateImplTest.java133
10 files changed, 1206 insertions, 4 deletions
diff --git a/server/sonar-cluster/pom.xml b/server/sonar-cluster/pom.xml
index 5ab47ff6fb2..291c1d4497e 100644
--- a/server/sonar-cluster/pom.xml
+++ b/server/sonar-cluster/pom.xml
@@ -28,14 +28,14 @@
<artifactId>guava</artifactId>
</dependency>
<dependency>
+ <groupId>commons-lang</groupId>
+ <artifactId>commons-lang</artifactId>
+ </dependency>
+ <dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>
- <dependency>
- <groupId>commons-lang</groupId>
- <artifactId>commons-lang</artifactId>
- </dependency>
<!-- unit tests -->
<dependency>
diff --git a/server/sonar-cluster/src/main/java/org/sonar/cluster/health/NodeDetails.java b/server/sonar-cluster/src/main/java/org/sonar/cluster/health/NodeDetails.java
new file mode 100644
index 00000000000..000030f8796
--- /dev/null
+++ b/server/sonar-cluster/src/main/java/org/sonar/cluster/health/NodeDetails.java
@@ -0,0 +1,200 @@
+/*
+ * 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.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * <p>{@link Externalizable} because this class is written to and from Hazelcast.</p>
+ */
+public class NodeDetails implements Externalizable {
+ private Type type;
+ private String name;
+ private String host;
+ private int port;
+ private long started;
+
+ /**
+ * Required for Serialization
+ */
+ public NodeDetails() {
+ }
+
+ private NodeDetails(Builder builder) {
+ this.type = builder.type;
+ this.name = builder.name;
+ this.host = builder.host;
+ this.port = builder.port;
+ this.started = builder.started;
+ }
+
+ public static Builder newNodeDetailsBuilder() {
+ return new Builder();
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public long getStarted() {
+ return started;
+ }
+
+ @Override
+ public String toString() {
+ return "NodeDetails{" +
+ "type=" + type +
+ ", name='" + name + '\'' +
+ ", host='" + host + '\'' +
+ ", port=" + port +
+ ", started=" + started +
+ '}';
+ }
+
+ @Override
+ public void writeExternal(ObjectOutput out) throws IOException {
+ out.writeInt(type.ordinal());
+ out.writeUTF(name);
+ out.writeUTF(host);
+ out.writeInt(port);
+ out.writeLong(started);
+ }
+
+ @Override
+ public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+ this.type = Type.values()[in.readInt()];
+ this.name = in.readUTF();
+ this.host = in.readUTF();
+ this.port = in.readInt();
+ this.started = in.readLong();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ NodeDetails that = (NodeDetails) o;
+ return port == that.port &&
+ started == that.started &&
+ type == that.type &&
+ name.equals(that.name) &&
+ host.equals(that.host);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(type, name, host, port, started);
+ }
+
+ public static class Builder {
+ private Type type;
+ private String name;
+ private String host;
+ private int port;
+ private long started;
+
+ private Builder() {
+ // use static factory method
+ }
+
+ public Builder setType(Type type) {
+ this.type = checkType(type);
+ return this;
+ }
+
+ public Builder setName(String name) {
+ this.name = checkString(name, "name");
+ return this;
+ }
+
+ public Builder setHost(String host) {
+ this.host = checkString(host, "host");
+ return this;
+ }
+
+ public Builder setPort(int port) {
+ checkPort(port);
+ this.port = port;
+ return this;
+ }
+
+ public Builder setStarted(long started) {
+ checkStarted(started);
+ this.started = started;
+ return this;
+ }
+
+ public NodeDetails build() {
+ checkType(type);
+ checkString(name, "name");
+ checkString(host, "host");
+ checkPort(port);
+ checkStarted(started);
+ return new NodeDetails(this);
+ }
+
+ private static Type checkType(Type type) {
+ return requireNonNull(type, "type can't be null");
+ }
+
+ private static String checkString(String name, String label) {
+ checkNotNull(name, "%s can't be null", label);
+ String value = name.trim();
+ checkArgument(!value.isEmpty(), "%s can't be empty", label);
+ return value;
+ }
+
+ private static void checkPort(int port) {
+ checkArgument(port > 0, "port must be > 0");
+ }
+
+ private static void checkStarted(long started) {
+ checkArgument(started > 0, "started must be > 0");
+ }
+ }
+
+ public enum Type {
+ APPLICATION, SEARCH
+ }
+}
diff --git a/server/sonar-cluster/src/main/java/org/sonar/cluster/health/NodeHealth.java b/server/sonar-cluster/src/main/java/org/sonar/cluster/health/NodeHealth.java
new file mode 100644
index 00000000000..b85f1c221f5
--- /dev/null
+++ b/server/sonar-cluster/src/main/java/org/sonar/cluster/health/NodeHealth.java
@@ -0,0 +1,189 @@
+/*
+ * 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 com.google.common.collect.ImmutableSet;
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * <p>{@link Externalizable} because this class is written to and from Hazelcast.</p>
+ */
+public class NodeHealth implements Externalizable {
+ private Status status;
+ private Set<String> causes;
+ private NodeDetails details;
+ private long date;
+
+ /**
+ * Required for Serialization
+ */
+ public NodeHealth() {
+ }
+
+ private NodeHealth(Builder builder) {
+ this.status = builder.status;
+ this.causes = ImmutableSet.copyOf(builder.causes);
+ this.details = builder.details;
+ this.date = builder.date;
+ }
+
+ public static Builder newNodeHealthBuilder() {
+ return new Builder();
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public Set<String> getCauses() {
+ return causes;
+ }
+
+ public NodeDetails getDetails() {
+ return details;
+ }
+
+ public long getDate() {
+ return date;
+ }
+
+ @Override
+ public void writeExternal(ObjectOutput out) throws IOException {
+ out.writeInt(status.ordinal());
+ out.writeInt(causes.size());
+ for (String cause : causes) {
+ out.writeUTF(cause);
+ }
+ out.writeObject(details);
+ out.writeLong(date);
+ }
+
+ @Override
+ public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+ this.status = Status.values()[in.readInt()];
+ int size = in.readInt();
+ if (size > 0) {
+ Set<String> readCauses = new HashSet<>(size);
+ for (int i = 0; i < size; i++) {
+ readCauses.add(in.readUTF());
+ }
+ this.causes = ImmutableSet.copyOf(readCauses);
+ } else {
+ this.causes = ImmutableSet.of();
+ }
+ this.details = (NodeDetails) in.readObject();
+ this.date = in.readLong();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ NodeHealth that = (NodeHealth) o;
+ return date == that.date &&
+ status == that.status &&
+ causes.equals(that.causes) &&
+ details.equals(that.details);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(status, causes, details, date);
+ }
+
+ @Override
+ public String toString() {
+ return "NodeHealth{" +
+ "status=" + status +
+ ", causes=" + causes +
+ ", details=" + details +
+ ", date=" + date +
+ '}';
+ }
+
+ public static class Builder {
+ private static final String STATUS_CANT_BE_NULL = "status can't be null";
+ private static final String DETAILS_CANT_BE_NULL = "details can't be null";
+ private static final String DATE_MUST_BE_MORE_THAN_0 = "date must be > 0";
+
+ private Status status;
+ private Set<String> causes = new HashSet<>(0);
+ private NodeDetails details;
+ private long date;
+
+ private Builder() {
+ // use static factory method
+ }
+
+ public Builder setStatus(Status status) {
+ this.status = requireNonNull(status, STATUS_CANT_BE_NULL);
+ return this;
+ }
+
+ public Builder clearCauses() {
+ this.causes.clear();
+ return this;
+ }
+
+ public Builder addCause(String cause) {
+ requireNonNull(cause, "cause can't be null");
+ String trimmed = cause.trim();
+ checkArgument(!trimmed.isEmpty(), "cause can't be empty");
+ causes.add(cause);
+ return this;
+ }
+
+ public Builder setDetails(NodeDetails details) {
+ requireNonNull(details, DETAILS_CANT_BE_NULL);
+ this.details = details;
+ return this;
+ }
+
+ public Builder setDate(long date) {
+ checkArgument(date > 0, DATE_MUST_BE_MORE_THAN_0);
+ this.date = date;
+ return this;
+ }
+
+ public NodeHealth build() {
+ requireNonNull(status, STATUS_CANT_BE_NULL);
+ requireNonNull(details, DETAILS_CANT_BE_NULL);
+ checkArgument(date > 0, DATE_MUST_BE_MORE_THAN_0);
+ return new NodeHealth(this);
+ }
+ }
+
+ public enum Status {
+ GREEN, YELLOW, RED
+ }
+}
diff --git a/server/sonar-cluster/src/main/java/org/sonar/cluster/health/NodeHealthProvider.java b/server/sonar-cluster/src/main/java/org/sonar/cluster/health/NodeHealthProvider.java
new file mode 100644
index 00000000000..9633e1a776c
--- /dev/null
+++ b/server/sonar-cluster/src/main/java/org/sonar/cluster/health/NodeHealthProvider.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+public interface NodeHealthProvider {
+ /**
+ * Returns the {@link NodeHealth} for the current SonarQube instance.
+ *
+ * <p>Implementation must support being called very frequently and from concurrent threads</p>
+ */
+ NodeHealth get();
+}
diff --git a/server/sonar-cluster/src/main/java/org/sonar/cluster/health/SharedHealthState.java b/server/sonar-cluster/src/main/java/org/sonar/cluster/health/SharedHealthState.java
new file mode 100644
index 00000000000..a714487eb70
--- /dev/null
+++ b/server/sonar-cluster/src/main/java/org/sonar/cluster/health/SharedHealthState.java
@@ -0,0 +1,34 @@
+/*
+ * 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.Set;
+
+public interface SharedHealthState {
+ /**
+ * Writes the {@link NodeHealth} of the current node to the shared health state.
+ */
+ void writeMine(NodeHealth nodeHealth);
+
+ /**
+ * Reads the {@link NodeHealth} of all nodes which shared to the shared health state.
+ */
+ Set<NodeHealth> readAll();
+}
diff --git a/server/sonar-cluster/src/main/java/org/sonar/cluster/health/SharedHealthStateImpl.java b/server/sonar-cluster/src/main/java/org/sonar/cluster/health/SharedHealthStateImpl.java
new file mode 100644
index 00000000000..32c178cab12
--- /dev/null
+++ b/server/sonar-cluster/src/main/java/org/sonar/cluster/health/SharedHealthStateImpl.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 com.google.common.collect.ImmutableSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.cluster.localclient.HazelcastClient;
+
+import static java.util.Objects.requireNonNull;
+
+public class SharedHealthStateImpl implements SharedHealthState {
+ private static final String SQ_HEALTH_STATE_REPLICATED_MAP_IDENTIFIER = "sq_health_state";
+ private static final Logger LOG = Loggers.get(SharedHealthStateImpl.class);
+
+ private final HazelcastClient hazelcastClient;
+
+ public SharedHealthStateImpl(HazelcastClient hazelcastClient) {
+ this.hazelcastClient = hazelcastClient;
+ }
+
+ @Override
+ public void writeMine(NodeHealth nodeHealth) {
+ requireNonNull(nodeHealth, "nodeHealth can't be null");
+
+ Map<String, NodeHealth> sqHealthState = hazelcastClient.getReplicatedMap(SQ_HEALTH_STATE_REPLICATED_MAP_IDENTIFIER);
+ if (LOG.isTraceEnabled()) {
+ LOG.trace("Reading {} and adding {}", new HashMap<>(sqHealthState), nodeHealth);
+ }
+ sqHealthState.put(hazelcastClient.getClientUUID(), nodeHealth);
+ }
+
+ @Override
+ public Set<NodeHealth> readAll() {
+ Map<String, NodeHealth> sqHealthState = hazelcastClient.getReplicatedMap(SQ_HEALTH_STATE_REPLICATED_MAP_IDENTIFIER);
+ if (LOG.isTraceEnabled()) {
+ LOG.trace("Reading {}", new HashMap<>(sqHealthState));
+ }
+ return ImmutableSet.copyOf(sqHealthState.values());
+ }
+}
diff --git a/server/sonar-cluster/src/main/java/org/sonar/cluster/health/package-info.java b/server/sonar-cluster/src/main/java/org/sonar/cluster/health/package-info.java
new file mode 100644
index 00000000000..f20bef983f4
--- /dev/null
+++ b/server/sonar-cluster/src/main/java/org/sonar/cluster/health/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.cluster.health;
+
+import javax.annotation.ParametersAreNonnullByDefault;
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
new file mode 100644
index 00000000000..1a1947c9c94
--- /dev/null
+++ b/server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeDetailsTest.java
@@ -0,0 +1,270 @@
+/*
+ * 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.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;
+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.newNodeDetailsBuilder;
+
+public class NodeDetailsTest {
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private Random random = new Random();
+ private NodeDetails.Type randomType = NodeDetails.Type.values()[random.nextInt(NodeDetails.Type.values().length)];
+ private NodeDetails.Builder builderUnderTest = newNodeDetailsBuilder();
+
+ @Test
+ public void setType_throws_NPE_if_arg_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("type can't be null");
+
+ builderUnderTest.setType(null);
+ }
+
+ @Test
+ public void setName_throws_NPE_if_arg_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("name can't be null");
+
+ builderUnderTest.setName(null);
+ }
+
+ @Test
+ public void setName_throws_IAE_if_arg_is_empty() {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("name can't be empty");
+
+ builderUnderTest.setName("");
+ }
+
+ @Test
+ public void setName_throws_IAE_if_arg_is_empty_after_trim() {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("name can't be empty");
+
+ builderUnderTest.setName(" ");
+ }
+
+ @Test
+ public void setHost_throws_NPE_if_arg_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("host can't be null");
+
+ builderUnderTest.setHost(null);
+ }
+
+ @Test
+ public void setHost_throws_IAE_if_arg_is_empty() {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("host can't be empty");
+
+ builderUnderTest.setHost("");
+ }
+
+ @Test
+ public void setHost_throws_IAE_if_arg_is_empty_after_trim() {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("host can't be empty");
+
+ builderUnderTest.setHost(" ");
+ }
+
+ @Test
+ public void setPort_throws_IAE_if_arg_is_less_than_1() {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("port must be > 0");
+
+ builderUnderTest.setPort(-random.nextInt(5));
+ }
+
+ @Test
+ public void setStarted_throws_IAE_if_arg_is_less_than_1() {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("started must be > 0");
+
+ builderUnderTest.setStarted(-random.nextInt(5));
+ }
+
+ @Test
+ public void build_throws_NPE_if_type_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("type can't be null");
+
+ builderUnderTest.build();
+ }
+
+ @Test
+ public void build_throws_NPE_if_name_is_null() {
+ builderUnderTest
+ .setType(randomType);
+
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("name can't be null");
+
+ builderUnderTest.build();
+ }
+
+ @Test
+ public void build_throws_NPE_if_host_is_null() {
+ builderUnderTest
+ .setType(randomType)
+ .setName(randomAlphanumeric(2));
+
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("host can't be null");
+
+ builderUnderTest.build();
+ }
+
+ @Test
+ public void build_throws_IAE_if_setPort_not_called() {
+ builderUnderTest
+ .setType(randomType)
+ .setName(randomAlphanumeric(2))
+ .setHost(randomAlphanumeric(3));
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("port must be > 0");
+
+ builderUnderTest.build();
+ }
+
+ @Test
+ public void build_throws_IAE_if_setStarted_not_called() {
+ builderUnderTest
+ .setType(randomType)
+ .setName(randomAlphanumeric(2))
+ .setHost(randomAlphanumeric(3))
+ .setPort(1 + random.nextInt(33));
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("started must be > 0");
+
+ builderUnderTest.build();
+ }
+
+ @Test
+ public void equals_is_based_on_content() {
+ NodeDetails.Builder builder = randomBuilder();
+
+ NodeDetails underTest = builder.build();
+
+ assertThat(underTest).isEqualTo(underTest);
+ assertThat(builder.build())
+ .isEqualTo(underTest)
+ .isNotSameAs(underTest);
+ assertThat(underTest).isNotEqualTo(null);
+ assertThat(underTest).isNotEqualTo(new Object());
+ }
+
+ @Test
+ public void hashcode_is_based_on_content() {
+ NodeDetails.Builder builder = randomBuilder();
+
+ NodeDetails underTest = builder.build();
+
+ assertThat(builder.build().hashCode())
+ .isEqualTo(underTest.hashCode());
+ }
+
+ @Test
+ public void NodeDetails_is_Externalizable() throws IOException, ClassNotFoundException {
+ NodeDetails source = randomNodeDetails();
+ byte[] byteArray = serialize(source);
+
+ NodeDetails underTest = (NodeDetails) new ObjectInputStream(new ByteArrayInputStream(byteArray)).readObject();
+
+ assertThat(underTest).isEqualTo(source);
+ }
+
+ @Test
+ public void verify_toString() {
+ String name = randomAlphanumeric(3);
+ String host = randomAlphanumeric(10);
+ int port = 1 + random.nextInt(10);
+ int started = 1 + random.nextInt(666);
+
+ NodeDetails underTest = builderUnderTest
+ .setType(randomType)
+ .setName(name)
+ .setHost(host)
+ .setPort(port)
+ .setStarted(started)
+ .build();
+
+ assertThat(underTest.toString())
+ .isEqualTo("NodeDetails{type=" + randomType + ", name='" + name + "', host='" + host + "', port=" + port + ", started=" + started + "}");
+ }
+
+ @Test
+ public void verify_getters() {
+ String name = randomAlphanumeric(3);
+ String host = randomAlphanumeric(10);
+ int port = 1 + random.nextInt(10);
+ int started = 1 + random.nextInt(666);
+
+ NodeDetails underTest = builderUnderTest
+ .setType(randomType)
+ .setName(name)
+ .setHost(host)
+ .setPort(port)
+ .setStarted(started)
+ .build();
+
+ assertThat(underTest.getType()).isEqualTo(randomType);
+ assertThat(underTest.getName()).isEqualTo(name);
+ assertThat(underTest.getHost()).isEqualTo(host);
+ 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/NodeHealthTest.java b/server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeHealthTest.java
new file mode 100644
index 00000000000..f1a899646ea
--- /dev/null
+++ b/server/sonar-cluster/src/test/java/org/sonar/cluster/health/NodeHealthTest.java
@@ -0,0 +1,263 @@
+/*
+ * 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.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;
+import org.junit.Rule;
+import org.junit.Test;
+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 {
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private Random random = new Random();
+ private NodeHealth.Status randomStatus = NodeHealth.Status.values()[random.nextInt(NodeHealth.Status.values().length)];
+ private NodeHealth.Builder builderUnderTest = newNodeHealthBuilder();
+
+ @Test
+ public void setStatus_throws_NPE_if_arg_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("status can't be null");
+
+ builderUnderTest.setStatus(null);
+ }
+
+ @Test
+ public void setDetails_throws_NPE_if_arg_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("details can't be null");
+
+ builderUnderTest.setDetails(null);
+ }
+
+ @Test
+ public void setDate_throws_IAR_if_arg_is_less_then_0() {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("date must be > 0");
+
+ builderUnderTest.setDate(-random.nextInt(22));
+ }
+
+ @Test
+ public void build_throws_NPE_if_status_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("status can't be null");
+
+ builderUnderTest.build();
+ }
+
+ @Test
+ public void build_throws_NPE_if_details_is_null() {
+ builderUnderTest.setStatus(randomStatus);
+
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("details can't be null");
+
+ builderUnderTest.build();
+ }
+
+ @Test
+ public void build_throws_IAE_if_date_is_less_than_1() {
+ builderUnderTest
+ .setStatus(randomStatus)
+ .setDetails(randomNodeDetails());
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("date must be > 0");
+
+ builderUnderTest.build();
+ }
+
+ @Test
+ public void clearClauses_clears_clauses_of_builder() {
+ NodeHealth.Builder underTest = randomBuilder();
+ NodeHealth original = underTest
+ .addCause(randomAlphanumeric(3))
+ .build();
+
+ underTest.clearCauses();
+
+ NodeHealth second = underTest.build();
+ assertThat(second.getStatus()).isEqualTo(original.getStatus());
+ assertThat(second.getDate()).isEqualTo(original.getDate());
+ assertThat(second.getDetails()).isEqualTo(original.getDetails());
+ assertThat(second.getCauses()).isEmpty();
+ }
+
+ @Test
+ public void builder_can_be_reused() {
+ NodeHealth.Builder builder = 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();
+ long newDate = 1 + random.nextInt(666);
+ builder
+ .clearCauses()
+ .setStatus(newRandomStatus)
+ .setDetails(newNodeDetails)
+ .setDate(newDate);
+ String[] newCauses = IntStream.range(0, 1 + random.nextInt(2)).mapToObj(i -> randomAlphanumeric(4)).toArray(String[]::new);
+ Arrays.stream(newCauses).forEach(builder::addCause);
+
+ NodeHealth newNodeHealth = builder.build();
+
+ assertThat(second).isEqualTo(original);
+ assertThat(newNodeHealth.getStatus()).isEqualTo(newRandomStatus);
+ assertThat(newNodeHealth.getDate()).isEqualTo(newDate);
+ assertThat(newNodeHealth.getDetails()).isEqualTo(newNodeDetails);
+ assertThat(newNodeHealth.getCauses()).containsOnly(newCauses);
+ }
+
+ @Test
+ public void equals_is_based_on_content() {
+ NodeHealth.Builder builder = randomBuilder();
+
+ NodeHealth underTest = builder.build();
+
+ assertThat(underTest).isEqualTo(underTest);
+ assertThat(builder.build())
+ .isEqualTo(underTest)
+ .isNotSameAs(underTest);
+ assertThat(underTest).isNotEqualTo(null);
+ assertThat(underTest).isNotEqualTo(new Object());
+ }
+
+ @Test
+ public void hashcode_is_based_on_content() {
+ NodeHealth.Builder builder = randomBuilder();
+
+ NodeHealth underTest = builder.build();
+
+ assertThat(builder.build().hashCode())
+ .isEqualTo(underTest.hashCode());
+ }
+
+ @Test
+ public void class_is_serializable_with_causes() throws IOException, ClassNotFoundException {
+ NodeHealth source = randomBuilder(1).build();
+ byte[] bytes = serialize(source);
+
+ NodeHealth underTest = (NodeHealth) new ObjectInputStream(new ByteArrayInputStream(bytes)).readObject();
+
+ assertThat(underTest).isEqualTo(source);
+ }
+
+ @Test
+ public void class_is_serializable_without_causes() throws IOException, ClassNotFoundException {
+ NodeHealth.Builder builder = newNodeHealthBuilder()
+ .setStatus(randomStatus)
+ .setDetails(randomNodeDetails())
+ .setDate(1 + random.nextInt(999));
+ NodeHealth source = builder.build();
+ byte[] bytes = 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();
+ int date = 1 + random.nextInt(999);
+ String cause = randomAlphanumeric(4);
+ NodeHealth.Builder builder = builderUnderTest
+ .setStatus(randomStatus)
+ .setDetails(nodeDetails)
+ .setDate(date)
+ .addCause(cause);
+
+ NodeHealth underTest = builder.build();
+
+ assertThat(underTest.toString())
+ .isEqualTo("NodeHealth{status=" + randomStatus + ", causes=[" + cause + "], details=" + nodeDetails + ", date=" + date + "}");
+ }
+
+ @Test
+ public void verify_getters() {
+ NodeDetails nodeDetails = randomNodeDetails();
+ int date = 1 + random.nextInt(999);
+ NodeHealth.Builder builder = builderUnderTest
+ .setStatus(randomStatus)
+ .setDetails(nodeDetails)
+ .setDate(date);
+ String[] causes = IntStream.range(0, random.nextInt(10)).mapToObj(i -> randomAlphanumeric(4)).toArray(String[]::new);
+ Arrays.stream(causes).forEach(builder::addCause);
+
+ NodeHealth underTest = builder.build();
+
+ assertThat(underTest.getStatus()).isEqualTo(randomStatus);
+ assertThat(underTest.getDetails()).isEqualTo(nodeDetails);
+ assertThat(underTest.getDate()).isEqualTo(date);
+ 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-cluster/src/test/java/org/sonar/cluster/health/SharedHealthStateImplTest.java b/server/sonar-cluster/src/test/java/org/sonar/cluster/health/SharedHealthStateImplTest.java
new file mode 100644
index 00000000000..3e3afce53ea
--- /dev/null
+++ b/server/sonar-cluster/src/test/java/org/sonar/cluster/health/SharedHealthStateImplTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.stream.IntStream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.Mockito;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.cluster.localclient.HazelcastClient;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.when;
+import static org.sonar.cluster.health.NodeDetails.newNodeDetailsBuilder;
+import static org.sonar.cluster.health.NodeHealth.newNodeHealthBuilder;
+
+public class SharedHealthStateImplTest {
+ private static final String MAP_SQ_HEALTH_STATE = "sq_health_state";
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ private final Random random = new Random();
+ private HazelcastClient hazelcastClient = Mockito.mock(HazelcastClient.class);
+ private SharedHealthStateImpl underTest = new SharedHealthStateImpl(hazelcastClient);
+
+ @Test
+ public void write_fails_with_NPE_if_arg_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("nodeHealth can't be null");
+
+ underTest.writeMine(null);
+ }
+
+ @Test
+ public void write_put_arg_into_map_sq_health_state_under_current_client_uuid() {
+ NodeHealth nodeHealth = randomNodeHealth();
+ Map<String, NodeHealth> map = new HashMap<>();
+ doReturn(map).when(hazelcastClient).getReplicatedMap(MAP_SQ_HEALTH_STATE);
+ String uuid = randomAlphanumeric(5);
+ when(hazelcastClient.getClientUUID()).thenReturn(uuid);
+
+ underTest.writeMine(nodeHealth);
+
+ assertThat(map.size()).isEqualTo(1);
+ assertThat(map.get(uuid)).isSameAs(nodeHealth);
+ assertThat(logTester.logs()).isEmpty();
+ }
+
+ @Test
+ public void write_logs_map_sq_health_state_content_and_NodeHealth_to_be_added_if_TRACE() {
+ logTester.setLevel(LoggerLevel.TRACE);
+ NodeHealth newNodeHealth = randomNodeHealth();
+ Map<String, NodeHealth> map = new HashMap<>();
+ map.put(randomAlphanumeric(4), randomNodeHealth());
+ doReturn(new HashMap<>(map)).when(hazelcastClient).getReplicatedMap(MAP_SQ_HEALTH_STATE);
+ String uuid = randomAlphanumeric(5);
+ when(hazelcastClient.getClientUUID()).thenReturn(uuid);
+
+ underTest.writeMine(newNodeHealth);
+
+ assertThat(logTester.logs()).hasSize(1);
+ assertThat(logTester.logs(LoggerLevel.TRACE).iterator().next()).isEqualTo("Reading " + map + " and adding " + newNodeHealth);
+
+ }
+
+ @Test
+ public void readAll_returns_all_NodeHealth_in_map_sq_health_state() {
+ NodeHealth[] expected = IntStream.range(0, 1 + random.nextInt(6)).mapToObj(i -> randomNodeHealth()).toArray(NodeHealth[]::new);
+ Map<String, NodeHealth> map = new HashMap<>();
+ String randomUuidBase = randomAlphanumeric(5);
+ for (int i = 0; i < expected.length; i++) {
+ map.put(randomUuidBase + i, expected[i]);
+ }
+ doReturn(map).when(hazelcastClient).getReplicatedMap(MAP_SQ_HEALTH_STATE);
+
+ assertThat(underTest.readAll()).containsOnly(expected);
+ assertThat(logTester.logs()).isEmpty();
+ }
+
+ @Test
+ public void readAll_logs_map_sq_health_state_content_if_TRACE() {
+ logTester.setLevel(LoggerLevel.TRACE);
+ Map<String, NodeHealth> map = new HashMap<>();
+ map.put(randomAlphanumeric(44), randomNodeHealth());
+ doReturn(map).when(hazelcastClient).getReplicatedMap(MAP_SQ_HEALTH_STATE);
+
+ underTest.readAll();
+
+ assertThat(logTester.logs()).hasSize(1);
+ assertThat(logTester.logs(LoggerLevel.TRACE).iterator().next()).isEqualTo("Reading " + map);
+ }
+
+ private NodeHealth randomNodeHealth() {
+ return newNodeHealthBuilder()
+ .setStatus(NodeHealth.Status.values()[random.nextInt(NodeHealth.Status.values().length)])
+ .setDetails(newNodeDetailsBuilder()
+ .setType(random.nextBoolean() ? NodeDetails.Type.SEARCH : NodeDetails.Type.APPLICATION)
+ .setName(randomAlphanumeric(30))
+ .setHost(randomAlphanumeric(10))
+ .setPort(1 + random.nextInt(666))
+ .setStarted(1 + random.nextInt(852))
+ .build())
+ .setDate(1 + random.nextInt(999))
+ .build();
+ }
+}