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

com.torodb.testing.mongodb.docker.AbstractReplicaSet Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2017 8Kdata Technology
 *
 * Licensed 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 com.torodb.testing.mongodb.docker;

import static com.torodb.testing.mongodb.docker.MongoRequestRetrier.retry;
import static com.torodb.testing.mongodb.docker.ServerAddressConverter.convert;

import com.google.common.net.HostAndPort;
import com.google.common.util.concurrent.AbstractIdleService;
import com.google.common.util.concurrent.ServiceManager;
import com.mongodb.MongoClient;
import com.mongodb.MongoServerException;
import com.mongodb.ServerAddress;
import com.torodb.testing.mongodb.docker.ReplicaSetConfig.SecondaryConfig;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bson.BsonArray;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
import org.bson.BsonDouble;
import org.bson.BsonInt32;
import org.bson.BsonInt64;
import org.bson.BsonString;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.IntSupplier;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 *
 */
abstract class AbstractReplicaSet extends AbstractIdleService implements ReplicaSet {
  private static final Logger LOGGER = LogManager.getLogger(AbstractReplicaSet.class);
  private final ReplicaSetConfig config;
  private final ServiceManager serviceManager;
  private Map mongosByAddress;
  private final Map mongosByConfig;
  private MongoClient replClient;

  AbstractReplicaSet(ReplicaSetConfig config, ReplMongodFactory nodeFactory) {
    this.config = config;
    this.mongosByConfig = createMongoByConfig(config, nodeFactory);
    this.serviceManager = new ServiceManager(mongosByConfig.values());
  }

  @Override
  public String getReplSetName() {
    return config.getReplSetName();
  }

  @Override
  protected void startUp() throws Exception {
    boolean correctStartup = false;

    try {
      serviceManager.startAsync();
      try {
        serviceManager.awaitHealthy();
      } catch (IllegalStateException ex) {
        serviceManager.servicesByState().get(State.FAILED).stream()
            .forEach(service -> LOGGER.error("Service " + service + " failed", failureCause()));
        throw ex;
      }

      this.mongosByAddress = mongosByConfig.values().stream()
          .collect(Collectors.toMap(
              ReplMongod::getAddress,
              Function.identity())
          );

      initiateReplicas();

      LOGGER.debug("Getting the replica set mongo client");

      replClient = new MongoClient(mongosByAddress.keySet().stream()
          .map(ServerAddressConverter::convert)
          .collect(Collectors.toList())
      );
      correctStartup = true;

      LOGGER.debug("The replication set with name {} has been started", getReplSetName());

    } finally {
      if (!correctStartup) {
        LOGGER.debug("Shutting down sub services after a failure at startup time");
        try {
          shutDown();
        } catch (Exception ex) {
          LOGGER.error("Ignored catched exception emergency shutdown");
        }
      }
    }
  }

  @Override
  protected void shutDown() throws Exception {
    replClient.close();

    serviceManager.stopAsync();
    serviceManager.awaitStopped();
  }

  @Override
  public Optional getPrimary() {
    checkRunning();
    ServerAddress primaryAddress = replClient.getReplicaSetStatus().getMaster();
    if (primaryAddress == null) {
      return Optional.empty();
    }
    HostAndPort primaryHostAndPort = convert(primaryAddress);

    ReplMongod primary = mongosByAddress.get(primaryHostAndPort);
    if (primary == null) {
      throw new AssertionError("Primary has the address " + primaryHostAndPort + " that is not "
          + "found on the nodes map");
    }
    return Optional.of(primary);
  }

  @Override
  public Map getMongosByAddress() {
    return Collections.unmodifiableMap(mongosByAddress);
  }

  @Override
  public Map getMongosByConfig() {
    return Collections.unmodifiableMap(mongosByConfig);
  }

  @Override
  public MongoClient getClient() {
    checkRunning();
    return replClient;
  }

  private void checkRunning() {
    if (!isRunning()) {
      throw new IllegalStateException("The replica set service is not running");
    }
  }

  private void initiateReplicas() throws InterruptedException {
    ReplMongod aMongo = mongosByAddress.values().stream().findAny().orElseThrow(
        () -> new AssertionError("At least one mongo is required")
    );

    Supplier task = () -> {
      aMongo.getMongoClient()
          .getDatabase("admin")
          .runCommand(createReplSetInitiateRequest());
      return null;
    };

    BiPredicate stopPredicate = (ex, attempt) -> {
      try {
        LOGGER.trace("Error while trying to initiate the replica set", ex);
        Thread.sleep(1000);
      } catch (InterruptedException ex1) {
        Thread.interrupted();
      }
      return attempt > 10;
    };

    retry(task, stopPredicate);
    
    waitForStableConfig();
  }
  
  private BsonDocument createReplSetInitiateRequest() {
    BsonDocument subDoc = new BsonDocument()
        .append("_id", new BsonString(config.getReplSetName()))
        .append("members", createMembersRequest());
    
    config.getProtocolVersion()
        .ifPresent(version -> subDoc.append("protocolVersion", new BsonInt64(version)));
    
    return new BsonDocument("replSetInitiate", subDoc);
  }

  private BsonArray createMembersRequest() {
    BsonArray array = new BsonArray();
    AtomicInteger counter = new AtomicInteger();
    IntSupplier idSupplier = counter::getAndIncrement;
    
    for (Map.Entry entry : mongosByConfig.entrySet()) {
      array.add(toMemberConfig(entry.getKey(), entry.getValue(), idSupplier));
    }

    return array;
  }

  private BsonDocument toMemberConfig(SecondaryConfig config, ReplMongod mongo,
      IntSupplier idSupplier) {
    BsonDocument memberConfig = new BsonDocument()
        .append("_id", new BsonInt32(idSupplier.getAsInt()))
        .append("host", new BsonString(mongo.getAddress().toString()));

    if (config.isArbiter()) {
      memberConfig.append("arbiterOnly", BsonBoolean.TRUE);
    } else {
      memberConfig.append("priority", new BsonDouble(config.getPriority()))
          .append("hidden", BsonBoolean.valueOf(config.isHidden()))
          .append("slaveDelay", new BsonInt64(config.getSecondsDelay()))
          .append("buildIndexes", BsonBoolean.valueOf(config.isBuildIndexes()))
          .append("votes", new BsonInt32(config.getVotes()));
    }
    return memberConfig;
  }

  private void waitForStableConfig() throws InterruptedException {
    int maxConfigVersion = 0;
    List staleMongos = new ArrayList<>(mongosByAddress.values());
    while (!staleMongos.isEmpty()) {
      ReplMongod choosedMongo = staleMongos.get(0);
      BsonDocument currentConfig = getReplicaSetConfig(choosedMongo);

      if (!currentConfig.containsKey("version")) {
        Thread.sleep(1000);
        continue;
      }
      
      int currentConfigVer = currentConfig.getNumber("version").intValue();
      if (currentConfigVer < maxConfigVersion) {
        LOGGER.debug("Mongo {} is still using the old config v{}", choosedMongo, currentConfigVer);
        Thread.sleep(1000);
        continue;
      }
      if (currentConfigVer == maxConfigVersion) {
        staleMongos.remove(0);
      } else {
        //We found a fresher repl config, so we have to start again
        LOGGER.debug("Mongo {} has a fresher repl config (v{}), asking all nodes again ",
            choosedMongo, currentConfigVer);
        assert currentConfigVer > maxConfigVersion;
        maxConfigVersion = currentConfigVer;
        staleMongos.clear();
        staleMongos.addAll(mongosByAddress.values());
      }
    }
    LOGGER.debug("Replica is using the stable config with version {}", maxConfigVersion);
  }
  
  private BsonDocument getReplicaSetConfig(ReplMongod mongo) {
    BsonDocument request = new BsonDocument("replSetGetConfig", new BsonDouble(1));

    Supplier requestExecution = () -> {
      BsonDocument reply = mongo.getMongoClient().getDatabase("admin")
          .runCommand(request, BsonDocument.class);
      return reply.getDocument("config");
    };

    BiPredicate stopPredicate = (ex, attempt) -> {
      LOGGER.trace("Error while trying to get the replica set config from "
          + mongo.getAddress() + " for " + attempt + "st time", ex);
      try {
        Thread.sleep(1000);

      } catch (InterruptedException ex1) {
        Thread.interrupted();
      }
      return attempt > 10;
    };

    return retry(requestExecution, stopPredicate);
  }

  private static Map createMongoByConfig(
      ReplicaSetConfig config, ReplMongodFactory nodeFactory) {
    return config.getSecondaries().stream()
        .collect(Collectors.toMap(
            Function.identity(),
            secondary -> nodeFactory.apply(config.getReplSetName(), secondary)
        ));
  }

  @FunctionalInterface
  public static interface ReplMongodFactory extends
      BiFunction {

    @Override
    public ReplMongod apply(String replSetName, SecondaryConfig config);

  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy