mongo-python-driver/pymongo/monitor.py
2014-10-23 16:30:55 -04:00

204 lines
6.7 KiB
Python

# Copyright 2009-2014 MongoDB, Inc.
#
# 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.
"""Class to monitor a MongoDB server on a background thread."""
import atexit
import socket
import threading
import time
import weakref
from pymongo import common, helpers, message, thread_util
from pymongo.server_type import SERVER_TYPE
from pymongo.ismaster import IsMaster
from pymongo.read_preferences import MovingAverage
from pymongo.server_description import ServerDescription
class Monitor(object):
def __init__(
self,
server_description,
topology,
pool,
topology_settings):
"""Class to monitor a MongoDB server on a background thread.
Pass an initial ServerDescription, a Topology, a Pool, and
TopologySettings.
The Topology is weakly referenced. The Pool must be exclusive to this
Monitor.
"""
super(Monitor, self).__init__()
self._server_description = server_description
self._topology = weakref.proxy(topology)
self._pool = pool
self._settings = topology_settings
self._stopped = False
self._event = thread_util.Event(self._settings.condition_class)
self._thread = None
def open(self):
"""Start monitoring, or restart after a fork.
Multiple calls have no effect.
"""
started = False
try:
started = self._thread and self._thread.is_alive()
except ReferenceError:
# Thread terminated.
pass
if not started:
thread = threading.Thread(target=self.run)
thread.daemon = True
self._thread = weakref.proxy(thread)
register_monitor(self)
thread.start()
def close(self):
"""Disconnect and stop monitoring.
The Monitor cannot be used after closing.
"""
self._stopped = True
self._pool.reset()
# Awake the thread so it notices that _stopped is True.
self.request_check()
def join(self, timeout=None):
if self._thread is not None:
try:
self._thread.join(timeout)
except ReferenceError:
# Thread already terminated.
pass
def request_check(self):
"""If the monitor is sleeping, wake and check the server soon."""
self._event.set()
def run(self):
while not self._stopped:
try:
self._server_description = self._check_with_retry()
self._topology.on_change(self._server_description)
except ReferenceError:
# Topology was garbage-collected.
self.close()
else:
start = time.time() # TODO: monotonic.
self._event.wait(common.HEARTBEAT_FREQUENCY)
self._event.clear()
wait_time = time.time() - start
if wait_time < common.MIN_HEARTBEAT_INTERVAL:
# request_check() was called before min_wait passed.
time.sleep(common.MIN_HEARTBEAT_INTERVAL - wait_time)
def _check_with_retry(self):
"""Call ismaster once or twice. Reset server's pool on error.
Returns a ServerDescription.
"""
# According to the spec, if an ismaster call fails we reset the
# server's pool. If a server was once connected, change its type
# to Unknown only after retrying once.
retry = self._server_description.server_type != SERVER_TYPE.Unknown
new_server_description = self._check_once()
if new_server_description:
return new_server_description
else:
self._topology.reset_pool(self._server_description.address)
if retry:
server_description = self._check_once()
if server_description:
return server_description
# Server type defaults to Unknown.
return ServerDescription(self._server_description.address)
def _check_once(self):
"""A single attempt to call ismaster.
Returns a ServerDescription, or None on error.
"""
try:
with self._pool.get_socket({}, 0, 0) as sock_info:
response, round_trip_time = self._check_with_socket(sock_info)
old_rtts = self._server_description.round_trip_times
if old_rtts:
new_rtts = old_rtts.clone_with(round_trip_time)
else:
new_rtts = MovingAverage([round_trip_time])
sd = ServerDescription(
self._server_description.address, response, new_rtts)
return sd
except socket.error:
return None
except Exception:
# TODO: This is unexpected. Log.
return None
def _check_with_socket(self, sock_info):
"""Return (IsMaster, round_trip_time).
Can raise socket.error or PyMongoError.
"""
# TODO: monotonic time.
start = time.time()
request_id, msg, _ = message.query(
0, 'admin.$cmd', 0, -1, {'ismaster': 1})
sock_info.send_message(msg)
raw_response = sock_info.receive_message(1, request_id)
result = helpers._unpack_response(raw_response)
return IsMaster(result['data'][0]), time.time() - start
# MONITORS has a weakref to each running Monitor. A Monitor is kept alive by
# a strong reference from its Server and its Thread. Once both are destroyed
# the Monitor is garbage-collected and removed from MONITORS. If, however,
# any threads are still running when the interpreter begins to shut down,
# we attempt to halt and join them to avoid spurious errors.
MONITORS = set()
def register_monitor(monitor):
ref = weakref.ref(monitor, _on_monitor_deleted)
MONITORS.add(ref)
def _on_monitor_deleted(ref):
MONITORS.remove(ref)
def shutdown_monitors():
# Keep a local copy of MONITORS as
# shutting down threads has a side effect
# of removing them from the MONITORS set()
monitors = list(MONITORS)
for ref in monitors:
monitor = ref()
if monitor:
monitor.close()
monitor.join(10)
atexit.register(shutdown_monitors)