mirror of
https://github.com/mongodb/mongo.git
synced 2024-11-24 16:46:00 +01:00
SERVER-18273 Compute ranges of ports for each job in resmoke.py
This commit is contained in:
parent
5fa5befcc7
commit
ff6326e5ab
@ -33,6 +33,7 @@ MONGO_RUNNER_SUBDIR = "mongorunner"
|
||||
|
||||
# Names below correspond to how they are specified via the command line or in the options YAML file.
|
||||
DEFAULTS = {
|
||||
"basePort": 20000,
|
||||
"buildloggerUrl": "https://logkeeper.mongodb.org",
|
||||
"continueOnFailure": False,
|
||||
"dbpathPrefix": None,
|
||||
@ -63,6 +64,10 @@ DEFAULTS = {
|
||||
# Variables that are set by the user at the command line or with --options.
|
||||
##
|
||||
|
||||
# The starting port number to use for mongod and mongos processes spawned by resmoke.py and the
|
||||
# mongo shell.
|
||||
BASE_PORT = None
|
||||
|
||||
# The root url of the buildlogger server.
|
||||
BUILDLOGGER_URL = None
|
||||
|
||||
|
@ -1,47 +1,114 @@
|
||||
"""
|
||||
Helper to reserve a network port.
|
||||
Class used to allocate ports for use by various mongod and mongos
|
||||
processes involved in running the tests.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import socket as _socket
|
||||
import collections
|
||||
import functools
|
||||
import threading
|
||||
|
||||
from .. import config
|
||||
from .. import errors
|
||||
|
||||
class UnusedPort(object):
|
||||
|
||||
def _check_port(func):
|
||||
"""
|
||||
Acquires an unused port from the OS.
|
||||
A decorator that verifies the port returned by the wrapped function
|
||||
is in the valid range.
|
||||
|
||||
Returns the port if it is valid, and raises a PortAllocationError
|
||||
otherwise.
|
||||
"""
|
||||
|
||||
# Use a set to keep track of ports that are acquired from the OS to avoid returning duplicates.
|
||||
# We do not remove ports from this set because they are used throughout the lifetime of
|
||||
# resmoke.py for started mongod/mongos processes.
|
||||
_ALLOCATED_PORTS = set()
|
||||
_ALLOCATED_PORTS_LOCK = threading.Lock()
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
port = func(*args, **kwargs)
|
||||
|
||||
def __init__(self):
|
||||
self.num = None
|
||||
self.__socket = None
|
||||
if port < 0:
|
||||
raise errors.PortAllocationError("Attempted to use a negative port")
|
||||
|
||||
def __enter__(self):
|
||||
while True:
|
||||
socket = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
|
||||
socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
|
||||
socket.bind(("0.0.0.0", 0))
|
||||
if port > PortAllocator.MAX_PORT:
|
||||
raise errors.PortAllocationError("Exhausted all available ports. Consider decreasing"
|
||||
" the number of jobs, or using a lower base port")
|
||||
|
||||
port = socket.getsockname()[1]
|
||||
with UnusedPort._ALLOCATED_PORTS_LOCK:
|
||||
# Check whether the OS has already given us 'port'.
|
||||
if port in UnusedPort._ALLOCATED_PORTS:
|
||||
socket.close()
|
||||
continue
|
||||
return port
|
||||
|
||||
UnusedPort._ALLOCATED_PORTS.add(port)
|
||||
return wrapper
|
||||
|
||||
self.num = port
|
||||
self.__socket = socket
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
if self.__socket is not None:
|
||||
self.__socket.close()
|
||||
class PortAllocator(object):
|
||||
"""
|
||||
This class is responsible for allocating ranges of ports.
|
||||
|
||||
It reserves a range of ports for each job with the first part of
|
||||
that range used for the fixture started by that job, and the second
|
||||
part of the range used for mongod and mongos processes started by
|
||||
tests run by that job.
|
||||
"""
|
||||
|
||||
# A PortAllocator will not return any port greater than this number.
|
||||
MAX_PORT = 2 ** 16 - 1
|
||||
|
||||
# Each job gets a contiguous range of _PORTS_PER_JOB ports, with job 0 getting the first block
|
||||
# of ports, job 1 getting the second block, and so on.
|
||||
_PORTS_PER_JOB = 30
|
||||
|
||||
# The first _PORTS_PER_FIXTURE ports of each range are reserved for the fixtures, the remainder
|
||||
# of the port range is used by tests.
|
||||
_PORTS_PER_FIXTURE = 10
|
||||
|
||||
_NUM_USED_PORTS_LOCK = threading.Lock()
|
||||
|
||||
# Used to keep track of how many ports a fixture has allocated.
|
||||
_NUM_USED_PORTS = collections.defaultdict(int)
|
||||
|
||||
@classmethod
|
||||
@_check_port
|
||||
def next_fixture_port(cls, job_num):
|
||||
"""
|
||||
Returns the next port for a fixture to use.
|
||||
|
||||
Raises a PortAllocationError if the fixture has requested more
|
||||
ports than are reserved per job, or if the next port is not a
|
||||
valid port number.
|
||||
"""
|
||||
with cls._NUM_USED_PORTS_LOCK:
|
||||
start_port = config.BASE_PORT + (job_num * cls._PORTS_PER_JOB)
|
||||
num_used_ports = cls._NUM_USED_PORTS[job_num]
|
||||
next_port = start_port + num_used_ports
|
||||
|
||||
cls._NUM_USED_PORTS[job_num] += 1
|
||||
|
||||
if next_port >= start_port + cls._PORTS_PER_FIXTURE:
|
||||
raise errors.PortAllocationError(
|
||||
"Fixture has requested more than the %d ports reserved per fixture"
|
||||
% cls._PORTS_PER_FIXTURE)
|
||||
|
||||
return next_port
|
||||
|
||||
@classmethod
|
||||
@_check_port
|
||||
def min_test_port(cls, job_num):
|
||||
"""
|
||||
For the given job, returns the lowest port that is reserved for
|
||||
use by tests.
|
||||
|
||||
Raises a PortAllocationError if that port is higher than the
|
||||
maximum port.
|
||||
"""
|
||||
return config.BASE_PORT + (job_num * cls._PORTS_PER_JOB) + cls._PORTS_PER_FIXTURE
|
||||
|
||||
@classmethod
|
||||
@_check_port
|
||||
def max_test_port(cls, job_num):
|
||||
"""
|
||||
For the given job, returns the highest port that is reserved
|
||||
for use by tests.
|
||||
|
||||
Raises a PortAllocationError if that port is higher than the
|
||||
maximum port.
|
||||
"""
|
||||
next_range_start = config.BASE_PORT + ((job_num + 1) * cls._PORTS_PER_JOB)
|
||||
return next_range_start - 1
|
||||
|
@ -33,3 +33,12 @@ class ServerFailure(TestFailure):
|
||||
as a failure.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class PortAllocationError(ResmokeError):
|
||||
"""
|
||||
Exception that is raised by the PortAllocator if a port is requested
|
||||
outside of the range of valid ports, or if a fixture requests more
|
||||
ports than were reserved for that job.
|
||||
"""
|
||||
pass
|
||||
|
@ -17,6 +17,7 @@ from .. import resmokeconfig
|
||||
# Mapping of the attribute of the parsed arguments (dest) to its key as it appears in the options
|
||||
# YAML configuration file. Most should only be converting from snake_case to camelCase.
|
||||
DEST_TO_CONFIG = {
|
||||
"base_port": "basePort",
|
||||
"buildlogger_url": "buildloggerUrl",
|
||||
"continue_on_failure": "continueOnFailure",
|
||||
"dbpath_prefix": "dbpathPrefix",
|
||||
@ -71,6 +72,11 @@ def parse_command_line():
|
||||
parser.add_option("--options", dest="options_file", metavar="OPTIONS",
|
||||
help="A YAML file that specifies global options to resmoke.py.")
|
||||
|
||||
parser.add_option("--basePort", dest="base_port", metavar="PORT",
|
||||
help=("The starting port number to use for mongod and mongos processes"
|
||||
" spawned by resmoke.py or the tests themselves. Each fixture and Job"
|
||||
" allocates a contiguous range of ports."))
|
||||
|
||||
parser.add_option("--buildloggerUrl", action="store", dest="buildlogger_url", metavar="URL",
|
||||
help="The root url of the buildlogger server.")
|
||||
|
||||
@ -186,6 +192,7 @@ def update_config_vars(values):
|
||||
if values[dest] is not None:
|
||||
config[config_var] = values[dest]
|
||||
|
||||
_config.BASE_PORT = int(config.pop("basePort"))
|
||||
_config.BUILDLOGGER_URL = config.pop("buildloggerUrl")
|
||||
_config.DBPATH_PREFIX = _expand_user(config.pop("dbpathPrefix"))
|
||||
_config.DBTEST_EXECUTABLE = _expand_user(config.pop("dbtest"))
|
||||
|
@ -266,8 +266,7 @@ class _MongoSFixture(interface.Fixture):
|
||||
self.mongos_options["chunkSize"] = 50
|
||||
|
||||
if "port" not in self.mongos_options:
|
||||
with core.network.UnusedPort() as port:
|
||||
self.mongos_options["port"] = port.num
|
||||
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,
|
||||
|
@ -69,8 +69,7 @@ class MongoDFixture(interface.Fixture):
|
||||
pass
|
||||
|
||||
if "port" not in self.mongod_options:
|
||||
with core.network.UnusedPort() as port:
|
||||
self.mongod_options["port"] = port.num
|
||||
self.mongod_options["port"] = core.network.PortAllocator.next_fixture_port(self.job_num)
|
||||
self.port = self.mongod_options["port"]
|
||||
|
||||
mongod = core.programs.mongod_program(self.logger,
|
||||
|
@ -307,6 +307,12 @@ class JSTestCase(TestCase):
|
||||
|
||||
global_vars["MongoRunner.dataDir"] = data_dir
|
||||
global_vars["MongoRunner.dataPath"] = data_path
|
||||
|
||||
min_port = core.network.PortAllocator.min_test_port(fixture.job_num)
|
||||
max_port = core.network.PortAllocator.max_test_port(fixture.job_num)
|
||||
global_vars["MongoRunner.minPort"] = min_port
|
||||
global_vars["MongoRunner.maxPort"] = max_port
|
||||
|
||||
self.shell_options["global_vars"] = global_vars
|
||||
|
||||
shutil.rmtree(data_dir, ignore_errors=True)
|
||||
|
Loading…
Reference in New Issue
Block a user