Skip to content

Commit f0daebf

Browse files
committed
PYTHON-799 Create a PeriodicExecutor class for background monitoring.
The executor will also be used to solve a deadlock in Cursor.__del__.
1 parent 34398d5 commit f0daebf

File tree

4 files changed

+165
-93
lines changed

4 files changed

+165
-93
lines changed

pymongo/monitor.py

Lines changed: 27 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,9 @@
1414

1515
"""Class to monitor a MongoDB server on a background thread."""
1616

17-
import atexit
18-
import threading
19-
import time
2017
import weakref
2118

22-
from pymongo import common, helpers, message, thread_util
19+
from pymongo import common, helpers, message, periodic_executor
2320
from pymongo.server_type import SERVER_TYPE
2421
from pymongo.ismaster import IsMaster
2522
from pymongo.monotonic import time as _time
@@ -42,7 +39,6 @@ def __init__(
4239
The Topology is weakly referenced. The Pool must be exclusive to this
4340
Monitor.
4441
"""
45-
super(Monitor, self).__init__()
4642
self._server_description = server_description
4743

4844
# A weakref callback, takes ref to the dead topology as its parameter.
@@ -52,70 +48,52 @@ def close(dummy):
5248
self._topology = weakref.proxy(topology, close)
5349
self._pool = pool
5450
self._settings = topology_settings
55-
self._stopped = False
56-
self._event = thread_util.Event(self._settings.condition_class)
57-
self._thread = None
5851
self._avg_round_trip_time = MovingAverage()
5952

53+
# We strongly reference the executor and it weakly references us via
54+
# this closure. When the monitor is freed, a call to target() raises
55+
# ReferenceError and stops the executor.
56+
def target():
57+
Monitor._run(weakref.proxy(self))
58+
59+
self._executor = periodic_executor.PeriodicExecutor(
60+
condition_class=self._settings.condition_class,
61+
interval=common.HEARTBEAT_FREQUENCY,
62+
min_interval=common.MIN_HEARTBEAT_INTERVAL,
63+
target=target)
64+
6065
def open(self):
6166
"""Start monitoring, or restart after a fork.
6267
6368
Multiple calls have no effect.
6469
"""
65-
self._stopped = False
66-
started = False
67-
try:
68-
started = self._thread and self._thread.is_alive()
69-
except ReferenceError:
70-
# Thread terminated.
71-
pass
72-
73-
if not started:
74-
thread = threading.Thread(target=self.run)
75-
thread.daemon = True
76-
self._thread = weakref.proxy(thread)
77-
register_monitor(self)
78-
thread.start()
70+
self._executor.open()
7971

8072
def close(self):
8173
"""Disconnect and stop monitoring.
8274
8375
open() restarts the monitor after closing.
8476
"""
85-
self._stopped = True
86-
self._pool.reset()
77+
self._executor.close()
8778

88-
# Wake the thread so it notices that _stopped is True.
89-
self.request_check()
79+
# Increment the pool_id and maybe close the socket. If the executor
80+
# thread has the socket checked out, it will be closed when checked in.
81+
self._pool.reset()
9082

9183
def join(self, timeout=None):
92-
if self._thread is not None:
93-
try:
94-
self._thread.join(timeout)
95-
except ReferenceError:
96-
# Thread already terminated.
97-
pass
84+
self._executor.join(timeout)
9885

9986
def request_check(self):
10087
"""If the monitor is sleeping, wake and check the server soon."""
101-
self._event.set()
88+
self._executor.wake()
10289

103-
def run(self):
104-
while not self._stopped:
105-
try:
106-
self._server_description = self._check_with_retry()
107-
self._topology.on_change(self._server_description)
108-
except ReferenceError:
109-
# Topology was garbage-collected.
110-
self.close()
111-
else:
112-
start = _time()
113-
self._event.wait(common.HEARTBEAT_FREQUENCY)
114-
self._event.clear()
115-
wait_time = _time() - start
116-
if wait_time < common.MIN_HEARTBEAT_INTERVAL:
117-
# request_check() was called before min interval passed.
118-
time.sleep(common.MIN_HEARTBEAT_INTERVAL - wait_time)
90+
def _run(self):
91+
try:
92+
self._server_description = self._check_with_retry()
93+
self._topology.on_change(self._server_description)
94+
except ReferenceError:
95+
# Topology was garbage-collected.
96+
self.close()
11997

12098
def _check_with_retry(self):
12199
"""Call ismaster once or twice. Reset server's pool on error.
@@ -175,34 +153,3 @@ def _check_with_socket(self, sock_info):
175153
raw_response = sock_info.receive_message(1, request_id)
176154
result = helpers._unpack_response(raw_response)
177155
return IsMaster(result['data'][0]), _time() - start
178-
179-
180-
# MONITORS has a weakref to each running Monitor. A Monitor is kept alive by
181-
# a strong reference from its Server and its Thread. Once both are destroyed
182-
# the Monitor is garbage-collected and removed from MONITORS. If, however,
183-
# any threads are still running when the interpreter begins to shut down,
184-
# we attempt to halt and join them to avoid spurious errors.
185-
MONITORS = set()
186-
187-
188-
def register_monitor(monitor):
189-
ref = weakref.ref(monitor, _on_monitor_deleted)
190-
MONITORS.add(ref)
191-
192-
193-
def _on_monitor_deleted(ref):
194-
MONITORS.remove(ref)
195-
196-
197-
def shutdown_monitors():
198-
# Keep a local copy of MONITORS as
199-
# shutting down threads has a side effect
200-
# of removing them from the MONITORS set()
201-
monitors = list(MONITORS)
202-
for ref in monitors:
203-
monitor = ref()
204-
if monitor:
205-
monitor.close()
206-
monitor.join(10)
207-
208-
atexit.register(shutdown_monitors)

pymongo/periodic_executor.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Copyright 2014 MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you
4+
# may not use this file except in compliance with the License. You
5+
# may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12+
# implied. See the License for the specific language governing
13+
# permissions and limitations under the License.
14+
15+
"""Run a target function on a background thread."""
16+
17+
import atexit
18+
import threading
19+
import time
20+
import weakref
21+
22+
from pymongo import thread_util
23+
24+
25+
class PeriodicExecutor(object):
26+
def __init__(self, condition_class, interval, min_interval, target):
27+
""""Run a target function periodically on a background thread.
28+
29+
If the function raises an exception, the executor stops.
30+
31+
:Parameters:
32+
- `condition_class`: A class like threading.Condition.
33+
- `interval`: Seconds between calls to `target`.
34+
- `min_interval`: Minimum seconds between calls if `wake` is
35+
called very often.
36+
- `target`: A function.
37+
"""
38+
self._event = thread_util.Event(condition_class)
39+
self._interval = interval
40+
self._min_interval = min_interval
41+
self._target = target
42+
self._stopped = False
43+
self._thread = None
44+
45+
def open(self):
46+
"""Start. Multiple calls have no effect.
47+
48+
Not safe to call from multiple threads at once.
49+
"""
50+
self._stopped = False
51+
started = False
52+
try:
53+
started = self._thread and self._thread.is_alive()
54+
except ReferenceError:
55+
# Thread terminated.
56+
pass
57+
58+
if not started:
59+
thread = threading.Thread(target=self._run)
60+
thread.daemon = True
61+
self._thread = weakref.proxy(thread)
62+
_register_executor(self)
63+
thread.start()
64+
65+
def close(self):
66+
"""Stop. To restart, call open()."""
67+
self._stopped = True
68+
69+
# Wake the thread so it notices that _stopped is True.
70+
self.wake()
71+
72+
def join(self, timeout=None):
73+
if self._thread is not None:
74+
try:
75+
self._thread.join(timeout)
76+
except ReferenceError:
77+
# Thread already terminated.
78+
pass
79+
80+
def wake(self):
81+
"""Execute the target function soon."""
82+
self._event.set()
83+
84+
def _run(self):
85+
while not self._stopped:
86+
try:
87+
self._target()
88+
except Exception:
89+
# It's the target's responsibility to report errors.
90+
self._stopped = True
91+
break
92+
93+
# Avoid running too frequently if wake() is called very often.
94+
time.sleep(self._min_interval)
95+
self._event.wait(self._interval - self._min_interval)
96+
self._event.clear()
97+
98+
99+
# _EXECUTORS has a weakref to each running PeriodicExecutor. Once started,
100+
# an executor is kept alive by a strong reference from its thread and perhaps
101+
# from other objects. When the thread dies and all other referrers are freed,
102+
# the executor is freed and removed from _EXECUTORS. If any threads are
103+
# running when the interpreter begins to shut down, we try to halt and join
104+
# them to avoid spurious errors.
105+
_EXECUTORS = set()
106+
107+
108+
def _register_executor(executor):
109+
ref = weakref.ref(executor, _on_executor_deleted)
110+
_EXECUTORS.add(ref)
111+
112+
113+
def _on_executor_deleted(ref):
114+
_EXECUTORS.remove(ref)
115+
116+
117+
def _shutdown_executors():
118+
# Copy the set. Stopping threads has the side effect of removing executors.
119+
executors = list(_EXECUTORS)
120+
for ref in executors:
121+
executor = ref()
122+
if executor:
123+
executor.close()
124+
executor.join(10)
125+
126+
atexit.register(_shutdown_executors)

test/test_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,13 +431,13 @@ def f(pipe):
431431
any_server_selector)
432432

433433
# In child, only the thread that called fork() is alive.
434-
assert not any(s._monitor._thread.is_alive()
434+
assert not any(s._monitor._executor._thread.is_alive()
435435
for s in servers)
436436

437437
db.test.find_one()
438438

439439
wait_until(
440-
lambda: all(s._monitor._thread.is_alive()
440+
lambda: all(s._monitor._executor._thread.is_alive()
441441
for s in servers),
442442
"restart monitor threads")
443443
except:

test/test_monitor.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,38 +20,37 @@
2020

2121
sys.path[0:0] = [""]
2222

23-
from pymongo.monitor import MONITORS
23+
from pymongo.periodic_executor import _EXECUTORS
2424
from test import unittest, port, host, IntegrationTest
2525
from test.utils import single_client, one, connected, wait_until
2626

2727

28-
def find_monitor_ref(monitor):
29-
for ref in MONITORS.copy():
30-
if ref() is monitor:
28+
def registered(executor):
29+
for ref in _EXECUTORS.copy():
30+
if ref() is executor:
3131
return ref
3232

3333
return None
3434

3535

3636
def unregistered(ref):
3737
gc.collect()
38-
return ref not in MONITORS
38+
return ref not in _EXECUTORS
3939

4040

4141
class TestMonitor(IntegrationTest):
4242
def test_atexit_hook(self):
4343
client = single_client(host, port)
44-
monitor = one(client._topology._servers.values())._monitor
44+
executor = one(client._topology._servers.values())._monitor._executor
4545
connected(client)
4646

47-
# The client registers a weakref to the monitor.
48-
ref = wait_until(partial(find_monitor_ref, monitor),
49-
'register monitor')
47+
# The executor stores a weakref to itself in _EXECUTORS.
48+
ref = wait_until(partial(registered, executor), 'register executor')
5049

51-
del monitor
50+
del executor
5251
del client
5352

54-
wait_until(partial(unregistered, ref), 'unregister monitor')
53+
wait_until(partial(unregistered, ref), 'unregister executor')
5554

5655

5756
if __name__ == "__main__":

0 commit comments

Comments
 (0)