diff options
author | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2017-09-05 16:08:54 +0200 |
---|---|---|
committer | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2017-09-13 15:50:50 +0200 |
commit | 893b3d2f8d5e5d2789cd5c954d8016742bfc18e5 (patch) | |
tree | 3486fa7c5d8e1a5bbfdcfbc9b1c3212be0ae7eda /server | |
parent | 128f5f74e20b7391331840493a899f8bec494059 (diff) | |
download | sonarqube-893b3d2f8d5e5d2789cd5c954d8016742bfc18e5.tar.gz sonarqube-893b3d2f8d5e5d2789cd5c954d8016742bfc18e5.zip |
SONAR-9741 add SharedHealthState
Diffstat (limited to 'server')
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(); + } +} |