Files
mongo/buildscripts/resmokelib/testing/fixtures/shardedcluster.py

352 lines
13 KiB
Python

"""
Sharded cluster fixture for executing JSTests against.
"""
from __future__ import absolute_import
import copy
import os.path
import socket
import time
import pymongo
from . import interface
from . import standalone
from . import replicaset
from ... import config
from ... import core
from ... import errors
from ... import logging
from ... import utils
class ShardedClusterFixture(interface.Fixture):
"""
Fixture which provides JSTests with a sharded cluster to run
against.
"""
_CONFIGSVR_REPLSET_NAME = "config-rs"
def __init__(self,
logger,
job_num,
mongos_executable=None,
mongos_options=None,
mongod_executable=None,
mongod_options=None,
dbpath_prefix=None,
preserve_dbpath=False,
num_shards=1,
separate_configsvr=True,
enable_sharding=None,
auth_options=None):
"""
Initializes ShardedClusterFixture with the different options to
the mongod and mongos processes.
"""
interface.Fixture.__init__(self, logger, job_num)
if "dbpath" in mongod_options:
raise ValueError("Cannot specify mongod_options.dbpath")
self.mongos_executable = mongos_executable
self.mongos_options = utils.default_if_none(mongos_options, {})
self.mongod_executable = mongod_executable
self.mongod_options = utils.default_if_none(mongod_options, {})
self.preserve_dbpath = preserve_dbpath
self.num_shards = num_shards
self.separate_configsvr = separate_configsvr
self.enable_sharding = utils.default_if_none(enable_sharding, [])
self.auth_options = auth_options
# Command line options override the YAML configuration.
dbpath_prefix = utils.default_if_none(config.DBPATH_PREFIX, dbpath_prefix)
dbpath_prefix = utils.default_if_none(dbpath_prefix, config.DEFAULT_DBPATH_PREFIX)
self._dbpath_prefix = os.path.join(dbpath_prefix,
"job%d" % (self.job_num),
config.FIXTURE_SUBDIR)
self.configsvr = None
self.mongos = None
self.shards = []
def setup(self):
if self.separate_configsvr:
if self.configsvr is None:
self.configsvr = self._new_configsvr()
self.configsvr.setup()
if not self.shards:
for i in xrange(self.num_shards):
shard = self._new_shard(i)
self.shards.append(shard)
# Start up each of the shards
for shard in self.shards:
shard.setup()
def await_ready(self):
# Wait for the config server
if self.configsvr is not None:
self.configsvr.await_ready()
# Wait for each of the shards
for shard in self.shards:
shard.await_ready()
if self.mongos is None:
self.mongos = self._new_mongos()
# Start up the mongos
self.mongos.setup()
# Wait for the mongos
self.mongos.await_ready()
self.port = self.mongos.port
client = utils.new_mongo_client(port=self.port)
if self.auth_options is not None:
auth_db = client[self.auth_options["authenticationDatabase"]]
auth_db.authenticate(self.auth_options["username"],
password=self.auth_options["password"],
mechanism=self.auth_options["authenticationMechanism"])
# Inform mongos about each of the shards
for shard in self.shards:
self._add_shard(client, shard)
# Enable sharding on each of the specified databases
for db_name in self.enable_sharding:
self.logger.info("Enabling sharding for '%s' database...", db_name)
client.admin.command({"enablesharding": db_name})
def teardown(self):
"""
Shuts down the sharded cluster.
"""
running_at_start = self.is_running()
success = True # Still a success even if nothing is running.
if not running_at_start:
self.logger.info("Sharded cluster was expected to be running in teardown(), but"
" wasn't.")
if self.configsvr is not None:
if running_at_start:
self.logger.info("Stopping config server...")
success = self.configsvr.teardown() and success
if running_at_start:
self.logger.info("Successfully terminated the config server.")
if self.mongos is not None:
if running_at_start:
self.logger.info("Stopping mongos...")
success = self.mongos.teardown() and success
if running_at_start:
self.logger.info("Successfully terminated the mongos.")
if running_at_start:
self.logger.info("Stopping shards...")
for shard in self.shards:
success = shard.teardown() and success
if running_at_start:
self.logger.info("Successfully terminated all shards.")
return success
def is_running(self):
"""
Returns true if the config server, all shards, and the mongos
are all still operating, and false otherwise.
"""
return (self.configsvr is not None and self.configsvr.is_running() and
all(shard.is_running() for shard in self.shards) and
self.mongos is not None and self.mongos.is_running())
def _new_configsvr(self):
"""
Returns a replicaset.ReplicaSetFixture configured to be used as
the config server of a sharded cluster.
"""
logger_name = "%s:configsvr" % (self.logger.name)
mongod_logger = logging.loggers.new_logger(logger_name, parent=self.logger)
mongod_options = copy.deepcopy(self.mongod_options)
mongod_options["configsvr"] = ""
mongod_options["dbpath"] = os.path.join(self._dbpath_prefix, "config")
mongod_options["replSet"] = ShardedClusterFixture._CONFIGSVR_REPLSET_NAME
mongod_options["storageEngine"] = "wiredTiger"
return replicaset.ReplicaSetFixture(mongod_logger,
self.job_num,
mongod_executable=self.mongod_executable,
mongod_options=mongod_options,
preserve_dbpath=self.preserve_dbpath,
num_nodes=3,
auth_options=self.auth_options,
replset_config_options={"configsvr": True})
def _new_shard(self, index):
"""
Returns a standalone.MongoDFixture configured to be used as a
shard in a sharded cluster.
"""
logger_name = "%s:shard%d" % (self.logger.name, index)
mongod_logger = logging.loggers.new_logger(logger_name, parent=self.logger)
mongod_options = copy.deepcopy(self.mongod_options)
mongod_options["shardsvr"] = ""
mongod_options["dbpath"] = os.path.join(self._dbpath_prefix, "shard%d" % (index))
return standalone.MongoDFixture(mongod_logger,
self.job_num,
mongod_executable=self.mongod_executable,
mongod_options=mongod_options,
preserve_dbpath=self.preserve_dbpath)
def _new_mongos(self):
"""
Returns a _MongoSFixture configured to be used as the mongos for
a sharded cluster.
"""
logger_name = "%s:mongos" % (self.logger.name)
mongos_logger = logging.loggers.new_logger(logger_name, parent=self.logger)
mongos_options = copy.deepcopy(self.mongos_options)
configdb_hostname = socket.gethostname()
if self.separate_configsvr:
configdb_replset = ShardedClusterFixture._CONFIGSVR_REPLSET_NAME
configdb_port = self.configsvr.port
mongos_options["configdb"] = "%s/%s:%d" % (configdb_replset,
configdb_hostname,
configdb_port)
else:
mongos_options["configdb"] = "%s:%d" % (configdb_hostname, self.shards[0].port)
return _MongoSFixture(mongos_logger,
self.job_num,
mongos_executable=self.mongos_executable,
mongos_options=mongos_options)
def _add_shard(self, client, shard):
"""
Add the specified program as a shard by executing the addShard
command.
See https://docs.mongodb.org/manual/reference/command/addShard
for more details.
"""
hostname = socket.gethostname()
self.logger.info("Adding %s:%d as a shard..." % (hostname, shard.port))
client.admin.command({"addShard": "%s:%d" % (hostname, shard.port)})
class _MongoSFixture(interface.Fixture):
"""
Fixture which provides JSTests with a mongos to connect to.
"""
def __init__(self,
logger,
job_num,
mongos_executable=None,
mongos_options=None):
interface.Fixture.__init__(self, logger, job_num)
# Command line options override the YAML configuration.
self.mongos_executable = utils.default_if_none(config.MONGOS_EXECUTABLE, mongos_executable)
self.mongos_options = utils.default_if_none(mongos_options, {}).copy()
self.mongos = None
def setup(self):
if "port" not in self.mongos_options:
self.mongos_options["port"] = core.network.PortAllocator.next_fixture_port(self.job_num)
self.port = self.mongos_options["port"]
mongos = core.programs.mongos_program(self.logger,
executable=self.mongos_executable,
**self.mongos_options)
try:
self.logger.info("Starting mongos on port %d...\n%s", self.port, mongos.as_command())
mongos.start()
self.logger.info("mongos started on port %d with pid %d.", self.port, mongos.pid)
except:
self.logger.exception("Failed to start mongos on port %d.", self.port)
raise
self.mongos = mongos
def await_ready(self):
deadline = time.time() + standalone.MongoDFixture.AWAIT_READY_TIMEOUT_SECS
# Wait until the mongos is accepting connections. The retry logic is necessary to support
# versions of PyMongo <3.0 that immediately raise a ConnectionFailure if a connection cannot
# be established.
while True:
# Check whether the mongos exited for some reason.
exit_code = self.mongos.poll()
if exit_code is not None:
raise errors.ServerFailure("Could not connect to mongos on port %d, process ended"
" unexpectedly with code %d." % (self.port, exit_code))
try:
# Use a shorter connection timeout to more closely satisfy the requested deadline.
client = utils.new_mongo_client(self.port, timeout_millis=500)
client.admin.command("ping")
break
except pymongo.errors.ConnectionFailure:
remaining = deadline - time.time()
if remaining <= 0.0:
raise errors.ServerFailure(
"Failed to connect to mongos on port %d after %d seconds"
% (self.port, standalone.MongoDFixture.AWAIT_READY_TIMEOUT_SECS))
self.logger.info("Waiting to connect to mongos on port %d.", self.port)
time.sleep(0.1) # Wait a little bit before trying again.
self.logger.info("Successfully contacted the mongos on port %d.", self.port)
def teardown(self):
running_at_start = self.is_running()
success = True # Still a success even if nothing is running.
if not running_at_start and self.port is not None:
self.logger.info("mongos on port %d was expected to be running in teardown(), but"
" wasn't." % (self.port))
if self.mongos is not None:
if running_at_start:
self.logger.info("Stopping mongos on port %d with pid %d...",
self.port,
self.mongos.pid)
self.mongos.stop()
exit_code = self.mongos.wait()
success = exit_code == 0
if running_at_start:
self.logger.info("Successfully terminated the mongos on port %d, exited with code"
" %d",
self.port,
exit_code)
return success
def is_running(self):
return self.mongos is not None and self.mongos.poll() is None