| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.brooklyn.entity.nosql.mongodb; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| import static com.google.common.base.Preconditions.checkNotNull; |
| |
| import java.util.Iterator; |
| |
| import org.apache.brooklyn.core.location.access.BrooklynAccessUtils; |
| import org.bson.BSONObject; |
| import org.bson.BasicBSONObject; |
| import org.bson.types.BasicBSONList; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import com.google.common.base.Optional; |
| import com.google.common.net.HostAndPort; |
| |
| /** |
| * Simplifies the creation of configuration objects for Mongo DB replica sets. |
| * <p/> |
| * A configuration object is structured like this: |
| * <pre> |
| * { |
| * "_id" : "replica-set-name", |
| * "version" : 3, |
| * "members" : [ |
| * { "_id" : 0, "host" : "Sams.local:27017" }, |
| * { "_id" : 1, "host" : "Sams.local:27018" }, |
| * { "_id" : 2, "host" : "Sams.local:27019" } |
| * ] |
| * } |
| * </pre> |
| * To add or remove servers to a replica set you must redefine this configuration |
| * (run <code>replSetReconfig</code> on the primary) with the new <code>members</code> |
| * list and the <code>version</code> updated. |
| */ |
| public class ReplicaSetConfig { |
| |
| private static final Logger LOG = LoggerFactory.getLogger(ReplicaSetConfig.class); |
| static final int MAXIMUM_REPLICA_SET_SIZE = 12; |
| static final int MAXIMUM_VOTING_MEMBERS = 7; |
| |
| private Optional<HostAndPort> primary = Optional.absent(); |
| |
| private String name; |
| private Integer version; |
| BasicBSONList members; |
| |
| public ReplicaSetConfig(String name) { |
| this(name, new BasicBSONList()); |
| } |
| |
| public ReplicaSetConfig(String name, BasicBSONList existingMembers) { |
| this.name = name; |
| this.members = existingMembers; |
| this.version = 1; |
| } |
| |
| /** |
| * Creates a configuration with the given name. |
| */ |
| public static ReplicaSetConfig builder(String name) { |
| return new ReplicaSetConfig(name); |
| } |
| |
| /** |
| * Creates a configuration from an existing configuration. |
| * <p/> |
| * Automatically increments the replica set's version number. |
| */ |
| public static ReplicaSetConfig fromExistingConfig(BSONObject config) { |
| checkNotNull(config); |
| checkArgument(config.containsField("_id"), "_id missing from replica set config"); |
| checkArgument(config.containsField("version"), "version missing from replica set config"); |
| checkArgument(config.containsField("members"), "members missing from replica set config"); |
| |
| String name = (String) config.get("_id"); |
| Integer version = (Integer) config.get("version"); |
| BasicBSONList members = (BasicBSONList) config.get("members"); |
| |
| return new ReplicaSetConfig(name, members).version(++version); |
| } |
| |
| /** |
| * Sets the version of the configuration. The version number must increase as the replica set changes. |
| */ |
| public ReplicaSetConfig version(Integer version) { |
| this.version = version; |
| return this; |
| } |
| |
| /** |
| * Notes the primary member of the replica. Primary members will always be voting members. |
| */ |
| public ReplicaSetConfig primary(HostAndPort primary) { |
| this.primary = Optional.of(primary); |
| return this; |
| } |
| |
| /** |
| * Adds a new member to the replica set config using {@link MongoDBServer#HOSTNAME} and {@link MongoDBServer#PORT} |
| * for hostname and port. Doesn't attempt to check that the id is free. |
| */ |
| public ReplicaSetConfig member(MongoDBServer server, Integer id) { |
| // TODO: Switch to SUBNET_HOSTNAME and there should be no need for a Brooklyn accessible |
| // address. It will require modification to MongoDBClientSupport, though, since it sets |
| // the primary to the host/port accessible from Brooklyn. |
| HostAndPort hap = BrooklynAccessUtils.getBrooklynAccessibleAddress(server, server.getAttribute(MongoDBServer.PORT)); |
| return member(hap.getHostText(), hap.getPort(), id); |
| } |
| |
| /** |
| * Adds a new member to the replica set config using the given {@link HostAndPort} for hostname and port. |
| * Doesn't attempt to check that the id is free. |
| */ |
| public ReplicaSetConfig member(HostAndPort address, Integer id) { |
| return member(address.getHostText(), address.getPort(), id); |
| } |
| |
| /** |
| * Adds a new member to the replica set config with the given hostname, port and id. Doesn't attempt to check |
| * that the id is free. |
| */ |
| public ReplicaSetConfig member(String hostname, Integer port, Integer id) { |
| if (members.size() == MAXIMUM_REPLICA_SET_SIZE) { |
| throw new IllegalStateException(String.format( |
| "Replica set {} exceeds maximum size of {} with addition of member at {}:{}", |
| new Object[]{name, MAXIMUM_REPLICA_SET_SIZE, hostname, port})); |
| } |
| BasicBSONObject member = new BasicBSONObject(); |
| member.put("_id", id); |
| member.put("host", String.format("%s:%s", hostname, port)); |
| members.add(member); |
| return this; |
| } |
| |
| /** Removes the first entity using {@link MongoDBServer#HOSTNAME} and {@link MongoDBServer#PORT}. */ |
| public ReplicaSetConfig remove(MongoDBServer server) { |
| HostAndPort hap = BrooklynAccessUtils.getBrooklynAccessibleAddress(server, server.getAttribute(MongoDBServer.PORT)); |
| return remove(hap.getHostText(), hap.getPort()); |
| } |
| |
| /** Removes the first entity with host and port matching the given address. */ |
| public ReplicaSetConfig remove(HostAndPort address) { |
| return remove(address.getHostText(), address.getPort()); |
| } |
| |
| /** |
| * Removes the first entity with the given hostname and port from the list of members |
| */ |
| public ReplicaSetConfig remove(String hostname, Integer port) { |
| String host = String.format("%s:%s", hostname, port); |
| Iterator<Object> it = this.members.iterator(); |
| while (it.hasNext()) { |
| Object next = it.next(); |
| if (next instanceof BasicBSONObject) { |
| BasicBSONObject basicBSONObject = (BasicBSONObject) next; |
| if (host.equals(basicBSONObject.getString("host"))) { |
| it.remove(); |
| break; |
| } |
| } |
| } |
| return this; |
| } |
| |
| /** |
| * @return A {@link BasicBSONObject} representing the configuration that is suitable for a MongoDB server. |
| */ |
| public BasicBSONObject build() { |
| setVotingMembers(); |
| BasicBSONObject config = new BasicBSONObject(); |
| config.put("_id", name); |
| config.put("version", version); |
| config.put("members", members); |
| return config; |
| } |
| |
| /** |
| * Selects 1, 3, 5 or 7 members to have a vote. The primary member (as set by |
| * {@link #primary(com.google.common.net.HostAndPort)}) is guaranteed a vote if |
| * it is in {@link #members}. |
| * <p/> |
| * |
| * Reconfiguring a server to be voters when they previously did not have votes generally triggers |
| * a primary election. This confuses the MongoDB Java driver, which logs an error like: |
| * <pre> |
| * WARN emptying DBPortPool to sams.home/192.168.1.64:27019 b/c of error |
| * java.io.EOFException: null |
| * at org.bson.io.Bits.readFully(Bits.java:48) ~[mongo-java-driver-2.11.3.jar:na] |
| * WARN Command { "replSetReconfig" : ... } on sams.home/192.168.1.64:27019 failed |
| * com.mongodb.MongoException$Network: Read operation to server sams.home/192.168.1.64:27019 failed on database admin |
| * at com.mongodb.DBTCPConnector.innerCall(DBTCPConnector.java:253) ~[mongo-java-driver-2.11.3.jar:na] |
| * Caused by: java.io.EOFException: null |
| * at org.bson.io.Bits.readFully(Bits.java:48) ~[mongo-java-driver-2.11.3.jar:na] |
| * </pre> |
| * |
| * The MongoDB documentation on <a href=http://docs.mongodb.org/manual/tutorial/configure-a-non-voting-replica-set-member/"> |
| * non-voting members</a> says: |
| * <blockquote> |
| * Initializes a new replica set configuration. Disconnects the shell briefly and forces a |
| * reconnection as the replica set renegotiates which member will be primary. As a result, |
| * the shell will display an error even if this command succeeds. |
| * </blockquote> |
| * |
| * So the problem is more that the MongoDB Java driver does not understand why the server |
| * may have disconnected and is to eager to report a problem. |
| */ |
| private void setVotingMembers() { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Setting voting and non-voting members of replica set: {}", name); |
| boolean seenPrimary = false; |
| String expectedPrimary = primary.isPresent() |
| ? primary.get().getHostText() + ":" + primary.get().getPort() |
| : ""; |
| |
| // Ensure an odd number of voters |
| int setSize = this.members.size(); |
| int nonPrimaryVotingMembers = Math.min(setSize % 2 == 0 ? setSize - 1 : setSize, MAXIMUM_VOTING_MEMBERS); |
| if (primary.isPresent()) { |
| if (LOG.isTraceEnabled()) |
| LOG.trace("Reserving vote for primary: " + expectedPrimary); |
| nonPrimaryVotingMembers -= 1; |
| } |
| |
| for (Object member : this.members) { |
| if (member instanceof BasicBSONObject) { |
| BasicBSONObject bsonObject = BasicBSONObject.class.cast(member); |
| String host = bsonObject.getString("host"); |
| |
| // is this member noted as the primary? |
| if (this.primary.isPresent() && expectedPrimary.equals(host)) { |
| bsonObject.put("votes", 1); |
| seenPrimary = true; |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Voting member (primary) of set {}: {}", name, host); |
| } else if (nonPrimaryVotingMembers-- > 0) { |
| bsonObject.put("votes", 1); |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Voting member of set {}: {}", name, host); |
| } else { |
| bsonObject.put("votes", 0); |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Non-voting member of set {}: {}", name, host); |
| } |
| } else { |
| LOG.error("Unexpected entry in replica set members list: " + member); |
| } |
| } |
| |
| if (primary.isPresent() && !seenPrimary) { |
| LOG.warn("Cannot give replica set primary a vote in reconfigured set: " + |
| "primary was indicated as {} but no member with that host and port was seen in the set. " + |
| "The replica set now has an even number of voters.", |
| this.primary); |
| } |
| } |
| |
| } |