All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.brooklyn.entity.nosql.mongodb.MongoDBReplicaSetImpl Maven / Gradle / Ivy

There is a newer version: 1.1.0
Show newest version
/*
 * 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 java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import javax.annotation.Nullable;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.policy.PolicySpec;
import org.apache.brooklyn.api.sensor.AttributeSensor;
import org.apache.brooklyn.api.sensor.SensorEvent;
import org.apache.brooklyn.api.sensor.SensorEventListener;
import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic;
import org.apache.brooklyn.core.entity.trait.Startable;
import org.apache.brooklyn.enricher.stock.Enrichers;
import org.apache.brooklyn.entity.group.AbstractMembershipTrackingPolicy;
import org.apache.brooklyn.entity.group.DynamicClusterImpl;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableSet;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;

/**
 * Implementation of {@link MongoDBReplicaSet}.
 *
 * Replica sets have a minimum of three members.
 *
 * Removal strategy is always {@link #NON_PRIMARY_REMOVAL_STRATEGY}.
 */
public class MongoDBReplicaSetImpl extends DynamicClusterImpl implements MongoDBReplicaSet {

    private static final Logger LOG = LoggerFactory.getLogger(MongoDBReplicaSetImpl.class);

    // Provides IDs for replica set members. The first member will have ID 0.
    private final AtomicInteger nextMemberId = new AtomicInteger(0);

    private MemberTrackingPolicy policy;
    private final AtomicBoolean mustInitialise = new AtomicBoolean(true);

    @SuppressWarnings("unchecked")
    protected static final List> SENSORS_TO_SUM = Arrays.asList(
        MongoDBServer.OPCOUNTERS_INSERTS,
        MongoDBServer.OPCOUNTERS_QUERIES,
        MongoDBServer.OPCOUNTERS_UPDATES,
        MongoDBServer.OPCOUNTERS_DELETES,
        MongoDBServer.OPCOUNTERS_GETMORE,
        MongoDBServer.OPCOUNTERS_COMMAND,
        MongoDBServer.NETWORK_BYTES_IN,
        MongoDBServer.NETWORK_BYTES_OUT,
        MongoDBServer.NETWORK_NUM_REQUESTS);
    
    public MongoDBReplicaSetImpl() {
    }

    /**
     * Manages member addition and removal.
     *
     * It's important that this is a single thread: the concurrent addition and removal
     * of members from the set would almost certainly have unintended side effects,
     * like reconfigurations using outdated ReplicaSetConfig instances.
     */
    private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

    /** true iff input is a non-null MongoDBServer with attribute REPLICA_SET_MEMBER_STATUS PRIMARY. */
    static final Predicate IS_PRIMARY = new Predicate() {
        // getPrimary relies on instanceof check
        @Override public boolean apply(@Nullable Entity input) {
            return input != null
                    && input instanceof MongoDBServer
                    && ReplicaSetMemberStatus.PRIMARY.equals(input.sensors().get(MongoDBServer.REPLICA_SET_MEMBER_STATUS));
        }
    };

    /** true iff. input is a non-null MongoDBServer with attribute REPLICA_SET_MEMBER_STATUS SECONDARY. */
    static final Predicate IS_SECONDARY = new Predicate() {
        @Override public boolean apply(@Nullable Entity input) {
            // getSecondaries relies on instanceof check
            return input != null
                    && input instanceof MongoDBServer
                    && ReplicaSetMemberStatus.SECONDARY.equals(input.sensors().get(MongoDBServer.REPLICA_SET_MEMBER_STATUS));
        }
    };

    /**
     * {@link Function} for use as the cluster's removal strategy. Chooses any entity with
     * {@link MongoDBServer#IS_PRIMARY_FOR_REPLICA_SET} true last of all.
     */
    private static final Function, Entity> NON_PRIMARY_REMOVAL_STRATEGY = new Function, Entity>() {
        @Override
        public Entity apply(@Nullable Collection entities) {
            checkArgument(entities != null && entities.size() > 0, "Expect list of MongoDBServers to have at least one entry");
            return Iterables.tryFind(entities, Predicates.not(IS_PRIMARY)).or(Iterables.get(entities, 0));
        }
    };
    
    @Override
    public void init() {
        super.init();
        enrichers().add(Enrichers.builder()
                .aggregating(MongoDBAuthenticationMixins.ROOT_USERNAME)
                .publishing(MongoDBAuthenticationMixins.ROOT_USERNAME)
                .fromMembers()
                .valueToReportIfNoSensors(null)
                .computing(new RootUsernameReducer())
                .build());
    }

    public static class RootUsernameReducer implements Function, String>{
        @Override
        public String apply(Collection input) {
            // when authentication is used all members have the same value
            return (input == null || input.isEmpty()) ? null : Iterables.getFirst(input, null);
        };
    }

    /** @return {@link #NON_PRIMARY_REMOVAL_STRATEGY} */
    @Override
    public Function, Entity> getRemovalStrategy() {
        return NON_PRIMARY_REMOVAL_STRATEGY;
    }

    @Override
    protected EntitySpec getMemberSpec() {
        EntitySpec spec = config().get(MEMBER_SPEC);
        if (spec == null) {
            spec = EntitySpec.create(MongoDBServer.class);
            config().set(MEMBER_SPEC, spec);
        }
        MongoDBAuthenticationUtils.setAuthenticationConfig(spec, this);
        return spec;
    }

    /**
     * Sets {@link MongoDBServer#REPLICA_SET}.
     */
    @Override
    protected Map getCustomChildFlags() {
        return ImmutableMap.builder()
                .putAll(super.getCustomChildFlags())
                .put(MongoDBServer.REPLICA_SET, getProxy())
                .build();
    }

    @Override
    public String getName() {
        // FIXME: Names must be unique if the replica sets are used in a sharded cluster
        return config().get(REPLICA_SET_NAME) + this.getId();
    }

    @Override
    public MongoDBServer getPrimary() {
        return Iterables.tryFind(getReplicas(), IS_PRIMARY).orNull();
    }

    @Override
    public Collection getSecondaries() {
        return FluentIterable.from(getReplicas())
                .filter(IS_SECONDARY)
                .toList();
    }

    @Override
    public Collection getReplicas() {
        return FluentIterable.from(getMembers())
                .transform(new Function() {
                    @Override public MongoDBServer apply(Entity input) {
                        return MongoDBServer.class.cast(input);
                    }
                })
                .toList();
    }

    /**
     * Initialises the replica set with the given server as primary if {@link #mustInitialise} is true,
     * otherwise schedules the addition of a new secondary.
     */
    private void serverAdded(MongoDBServer server) {
        try {
            LOG.debug("Server added: {}. SERVICE_UP: {}", server, server.sensors().get(MongoDBServer.SERVICE_UP));

            // Set the primary if the replica set hasn't been initialised.
            if (mustInitialise.compareAndSet(true, false)) {
                if (LOG.isInfoEnabled())
                    LOG.info("First server up in {} is: {}", getName(), server);
                boolean replicaSetInitialised = server.initializeReplicaSet(getName(), nextMemberId.getAndIncrement());
                if (replicaSetInitialised) {
                    sensors().set(PRIMARY_ENTITY, server);
                    sensors().set(Startable.SERVICE_UP, true);
                } else {
                    ServiceStateLogic.ServiceNotUpLogic.updateNotUpIndicator(this, "initialization", "replicaset failed to initialize");
                    ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE);
                }
            } else {
                if (LOG.isDebugEnabled())
                    LOG.debug("Scheduling addition of member to {}: {}", getName(), server);
                addSecondaryWhenPrimaryIsNonNull(server);
            }
        } catch (Exception e) {
            ServiceStateLogic.ServiceNotUpLogic.updateNotUpIndicator(server, "Failed to update replicaset", e);
        }
    }

    /**
     * Adds a server as a secondary in the replica set.
     * 

* If {@link #getPrimary} returns non-null submit the secondary to the primary's * {@link MongoDBClientSupport}. Otherwise, reschedule the task to run again in three * seconds time (in the hope that next time the primary will be available). */ private void addSecondaryWhenPrimaryIsNonNull(final MongoDBServer secondary) { // TODO Don't use executor, use ExecutionManager executor.submit(new Runnable() { @Override public void run() { // SERVICE_UP is not guaranteed when additional members are added to the set. Boolean isAvailable = secondary.sensors().get(MongoDBServer.SERVICE_UP); MongoDBServer primary = getPrimary(); boolean reschedule; if (Boolean.TRUE.equals(isAvailable) && primary != null) { boolean added = primary.addMemberToReplicaSet(secondary, nextMemberId.incrementAndGet()); if (added) { LOG.info("{} added to replica set {}", secondary, getName()); reschedule = false; } else { if (LOG.isDebugEnabled()) { LOG.debug("{} could not be added to replica set via {}; rescheduling", secondary, getName()); } reschedule = true; } } else { if (LOG.isTraceEnabled()) { LOG.trace("Rescheduling addition of member {} to replica set {}: service_up={}, primary={}", new Object[] {secondary, getName(), isAvailable, primary}); } reschedule = true; } if (reschedule) { // TODO Could limit number of retries executor.schedule(this, 3, TimeUnit.SECONDS); } } }); } /** * Removes a server from the replica set. *

* Submits a task that waits for the member to be down and for the replica set to have a primary * member, then reconfigures the set to remove the member, to {@link #executor}. If either of the * two conditions are not met then the task reschedules itself. * * @param member The server to be removed from the replica set. */ private void serverRemoved(final MongoDBServer member) { try { if (LOG.isDebugEnabled()) LOG.debug("Scheduling removal of member from {}: {}", getName(), member); // FIXME is there a chance of race here? if (member.equals(sensors().get(PRIMARY_ENTITY))) sensors().set(PRIMARY_ENTITY, null); executor.submit(new Runnable() { @Override public void run() { // Wait until the server has been stopped before reconfiguring the set. Quoth the MongoDB doc: // for best results always shut down the mongod instance before removing it from a replica set. Boolean isAvailable = member.sensors().get(MongoDBServer.SERVICE_UP); // Wait for the replica set to elect a new primary if the set is reconfiguring itself. MongoDBServer primary = getPrimary(); boolean reschedule; if (primary != null && !isAvailable) { boolean removed = primary.removeMemberFromReplicaSet(member); if (removed) { LOG.info("Removed {} from replica set {}", member, getName()); reschedule = false; } else { if (LOG.isDebugEnabled()) { LOG.debug("{} could not be removed from replica set via {}; rescheduling", member, getName()); } reschedule = true; } } else { if (LOG.isTraceEnabled()) { LOG.trace("Rescheduling removal of member {} from replica set {}: service_up={}, primary={}", new Object[]{member, getName(), isAvailable, primary}); } reschedule = true; } if (reschedule) { // TODO Could limit number of retries executor.schedule(this, 3, TimeUnit.SECONDS); } } }); } catch (Exception e) { ServiceStateLogic.ServiceNotUpLogic.updateNotUpIndicator(member, "Failed to update replicaset", e); } } @Override public void start(Collection locations) { // Promises that all the cluster's members have SERVICE_UP true on returning. super.start(locations); policy = policies().add(PolicySpec.create(MemberTrackingPolicy.class) .displayName(getName() + " membership tracker") .configure("group", this)); for (AttributeSensor sensor: SENSORS_TO_SUM) enrichers().add(Enrichers.builder() .aggregating(sensor) .publishing(sensor) .fromMembers() .computingSum() .valueToReportIfNoSensors(null) .defaultValueForUnreportedSensors(null) .build()); // FIXME would it be simpler to have a *subscription* on four or five sensors on allMembers, including SERVICE_UP // (which we currently don't check), rather than an enricher, and call to an "update" method? enrichers().add(Enrichers.builder() .aggregating(MongoDBServer.REPLICA_SET_PRIMARY_ENDPOINT) .publishing(MongoDBServer.REPLICA_SET_PRIMARY_ENDPOINT) .fromMembers() .valueToReportIfNoSensors(null) .computing(new Function, String>() { @Override public String apply(Collection input) { if (input==null || input.isEmpty()) return null; Set distinct = MutableSet.of(); for (String endpoint: input) if (!Strings.isBlank(endpoint)) distinct.add(endpoint); if (distinct.size()>1) LOG.warn("Mongo replica set "+MongoDBReplicaSetImpl.this+" detetcted multiple masters (transitioning?): "+distinct); return input.iterator().next(); }}) .build()); enrichers().add(Enrichers.builder() .aggregating(MongoDBServer.MONGO_SERVER_ENDPOINT) .publishing(REPLICA_SET_ENDPOINTS) .fromMembers() .valueToReportIfNoSensors(null) .computing(new Function, List>() { @Override public List apply(Collection input) { Set endpoints = new TreeSet(); for (String endpoint: input) { if (!Strings.isBlank(endpoint)) { endpoints.add(endpoint); } } return MutableList.copyOf(endpoints); }}) .build()); enrichers().add(Enrichers.builder() .transforming(REPLICA_SET_ENDPOINTS) .publishing(DATASTORE_URL) .computing(new EndpointsToDatastoreUrlMapper(this)) .build()); subscriptions().subscribeToMembers(this, MongoDBServer.IS_PRIMARY_FOR_REPLICA_SET, new SensorEventListener() { @Override public void onEvent(SensorEvent event) { if (Boolean.TRUE == event.getValue()) sensors().set(PRIMARY_ENTITY, (MongoDBServer)event.getSource()); } }); } public static class EndpointsToDatastoreUrlMapper implements Function, String> { private Entity entity; public EndpointsToDatastoreUrlMapper(Entity entity) { this.entity = entity; } @Override public String apply(Collection input) { String credentials = MongoDBAuthenticationUtils.usesAuthentication(entity) ? String.format("%s:%s@", entity.config().get(MongoDBAuthenticationMixins.ROOT_USERNAME), entity.config().get(MongoDBAuthenticationMixins.ROOT_PASSWORD)) : ""; return String.format("mongodb://%s%s", credentials, Strings.join(input, ",")); } } @Override public void stop() { // Do we want to remove the members from the replica set? // - if the set is being stopped forever it's irrelevant // - if the set might be restarted I think it just inconveniences us // Terminate the executor immediately. // TODO Note that after this the executor will not run if the set is restarted. executor.shutdownNow(); super.stop(); sensors().set(Startable.SERVICE_UP, false); } @Override public void onManagementStopped() { super.onManagementStopped(); executor.shutdownNow(); } public static class MemberTrackingPolicy extends AbstractMembershipTrackingPolicy { @Override protected void onEntityChange(Entity member) { // Ignored } @Override protected void onEntityAdded(Entity member) { ((MongoDBReplicaSetImpl) entity).serverAdded((MongoDBServer) member); } @Override protected void onEntityRemoved(Entity member) { ((MongoDBReplicaSetImpl) entity).serverRemoved((MongoDBServer) member); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy