summaryrefslogtreecommitdiff
path: root/buildscripts/resmokelib/testing/fixtures/standalone.py
blob: 98e18241df83530010e5340c21b71864092a130f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
"""
Standalone mongod fixture for executing JSTests against.
"""

from __future__ import absolute_import

import os
import os.path
import socket
import time

import pymongo

from . import interface
from ... import config
from ... import core
from ... import errors
from ... import utils


class MongoDFixture(interface.Fixture):
    """
    Fixture which provides JSTests with a standalone mongod to run
    against.
    """

    AWAIT_READY_TIMEOUT_SECS = 300

    def __init__(self,
                 logger,
                 job_num,
                 mongod_executable=None,
                 mongod_options=None,
                 dbpath_prefix=None,
                 preserve_dbpath=False):

        interface.Fixture.__init__(self, logger, job_num)

        if "dbpath" in mongod_options and dbpath_prefix is not None:
            raise ValueError("Cannot specify both mongod_options.dbpath and dbpath_prefix")

        # Command line options override the YAML configuration.
        self.mongod_executable = utils.default_if_none(config.MONGOD_EXECUTABLE, mongod_executable)

        self.mongod_options = utils.default_if_none(mongod_options, {}).copy()
        self.preserve_dbpath = preserve_dbpath

        # The dbpath in mongod_options takes precedence over other settings to make it easier for
        # users to specify a dbpath containing data to test against.
        if "dbpath" not in self.mongod_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.mongod_options["dbpath"] = os.path.join(dbpath_prefix,
                                                         "job%d" % (self.job_num),
                                                         config.FIXTURE_SUBDIR)
        self._dbpath = self.mongod_options["dbpath"]

        self.mongod = None

    def reconfigure(self, mongod_executable, mongod_options):
        self.original_mongod_executable = self.mongod_executable
        self.original_mongod_options = self.mongod_options
        self.original_preserve_dbpath = self.preserve_dbpath

        teardown_success = self.teardown()

        self.mongod_executable = mongod_executable
        self.mongod_options = mongod_options
        self.preserve_dbpath = True 

        self.setup()
        self.await_ready()
        
        if not teardown_success:
            raise errors.TestFailure("%s did not exit cleanly" % (self))

    def reset_configuration(self):
        teardown_success = self.teardown()

        self.mongod_executable = self.original_mongod_executable
        self.mongod_options = self.original_mongod_options

        self.setup()
        self.await_ready()

        # Reset preserve_dbpath after calling setup(), since setup() should always preserve the
        # dbpath.
        self.preserve_dbpath = self.original_preserve_dbpath
        
        if not teardown_success:
            raise errors.TestFailure("%s did not exit cleanly" % (self))

    def setup(self):
        if not self.preserve_dbpath:
            utils.rmtree(self._dbpath, ignore_errors=True)

        try:
            os.makedirs(self._dbpath)
        except os.error:
            # Directory already exists.
            pass

        if "port" not in self.mongod_options:
            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,
                                              executable=self.mongod_executable,
                                              **self.mongod_options)
        try:
            self.logger.info("Starting mongod on port %d...\n%s", self.port, mongod.as_command())
            mongod.start()
            self.logger.info("mongod started on port %d with pid %d.", self.port, mongod.pid)
        except:
            self.logger.exception("Failed to start mongod on port %d.", self.port)
            raise

        self.mongod = mongod

    def await_ready(self):
        deadline = time.time() + MongoDFixture.AWAIT_READY_TIMEOUT_SECS

        # Wait until the mongod 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 mongod exited for some reason.
            exit_code = self.mongod.poll()
            if exit_code is not None:
                raise errors.ServerFailure("Could not connect to mongod 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 mongod on port %d after %d seconds"
                        % (self.port, MongoDFixture.AWAIT_READY_TIMEOUT_SECS))

                self.logger.info("Waiting to connect to mongod on port %d.", self.port)
                time.sleep(0.1)  # Wait a little bit before trying again.

        self.logger.info("Successfully contacted the mongod 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("mongod on port %d was expected to be running in teardown(), but"
                             " wasn't." % (self.port))

        if self.mongod is not None:
            if running_at_start:
                self.logger.info("Stopping mongod on port %d with pid %d...",
                                 self.port,
                                 self.mongod.pid)
                self.mongod.stop()

            exit_code = self.mongod.wait()
            success = exit_code == 0

            if running_at_start:
                self.logger.info("Successfully terminated the mongod on port %d, exited with code"
                                 " %d.",
                                 self.port,
                                 exit_code)

        return success

    def is_running(self):
        return self.mongod is not None and self.mongod.poll() is None

    def get_internal_connection_string(self):
        if self.mongod is None:
            raise ValueError("Must call setup() before calling get_internal_connection_string()")

        return "%s:%d" % (socket.gethostname(), self.port)

    def get_driver_connection_url(self):
        return "mongodb://" + self.get_internal_connection_string()