/* * 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); } } }