Skip to content

Commit e6457cd

Browse files
committed
PYTHON-983 Update docs for new executor design.
1 parent 4618998 commit e6457cd

File tree

5 files changed

+69
-21
lines changed

5 files changed

+69
-21
lines changed

doc/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Highlights include:
1717
`tzinfo` option of :class:`~bson.codec_options.CodecOptions`.
1818
- An implementation of :class:`~gridfs.GridFSBucket` from the new GridFS spec.
1919
- Compliance with the new Connection String spec.
20+
- Reduced idle CPU usage in Python 2.
2021

2122
Changes in internal classes
2223
...........................

doc/developer/periodic_executor.rst

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,6 @@ PyMongo implements a :class:`~periodic_executor.PeriodicExecutor` for two
77
purposes: as the background thread for :class:`~monitor.Monitor`, and to
88
regularly check if there are `OP_KILL_CURSORS` messages that must be sent to the server.
99

10-
Monitoring
11-
----------
12-
13-
For each server in the topology, :class:`~topology.Topology` launches a
14-
monitor thread. This thread must not prevent the topology from being freed,
15-
so it weakrefs the topology. Furthermore, it uses a weakref callback to close
16-
itself promptly when the topology is freed.
17-
18-
Solid lines represent strong references, dashed lines weak ones:
19-
20-
.. generated with graphviz from periodic-executor-refs.dot
21-
22-
.. image:: ../static/periodic-executor-refs.png
23-
24-
See `Stopping Executors`_ below for an explanation of ``_EXECUTORS``.
25-
2610
Killing Cursors
2711
---------------
2812

@@ -35,16 +19,17 @@ the cursor before finishing iteration::
3519

3620
We try to send an `OP_KILL_CURSORS` to the server to tell it to clean up the
3721
server-side cursor. But we must not take any locks directly from the cursor's
38-
destructor (see `PYTHON-799 <https://jira.mongodb.org/browse/PYTHON-799>`_),
39-
so we cannot safely use the PyMongo data structures required to send a message.
40-
The solution is to add the cursor's id to an array on the
41-
:class:`~mongo_client.MongoClient` without taking any locks.
22+
destructor (see `PYTHON-799`_), so we cannot safely use the PyMongo data
23+
structures required to send a message. The solution is to add the cursor's id
24+
to an array on the :class:`~mongo_client.MongoClient` without taking any locks.
4225

4326
Each client has a :class:`~periodic_executor.PeriodicExecutor` devoted to
4427
checking the array for cursor ids. Any it sees are the result of cursors that
4528
were freed while the server-side cursor was still open. The executor can safely
4629
take the locks it needs in order to send the `OP_KILL_CURSORS` message.
4730

31+
.. _PYTHON-799: https://jira.mongodb.org/browse/PYTHON-799
32+
4833
Stopping Executors
4934
------------------
5035

@@ -55,7 +40,7 @@ the topology calls :meth:`close` on all its monitor threads, the :meth:`close`
5540
method cannot actually call :meth:`wake` on the executor, since :meth:`wake`
5641
takes a lock.
5742

58-
Instead, executors wake very frequently to check if ``self.close`` is set,
43+
Instead, executors wake periodically to check if ``self.close`` is set,
5944
and if so they exit.
6045

6146
A thread can log spurious errors if it wakes late in the Python interpreter's
@@ -67,3 +52,62 @@ An `exit handler`_ runs on shutdown and tells all executors to stop, then
6752
tries (with a short timeout) to join all executor threads.
6853

6954
.. _exit handler: https://docs.python.org/2/library/atexit.html
55+
56+
Monitoring
57+
----------
58+
59+
For each server in the topology, :class:`~topology.Topology` uses a periodic
60+
executor to launch a monitor thread. This thread must not prevent the topology
61+
from being freed, so it weakrefs the topology. Furthermore, it uses a weakref
62+
callback to terminate itself soon after the topology is freed.
63+
64+
Solid lines represent strong references, dashed lines weak ones:
65+
66+
.. generated with graphviz: "dot -Tpng periodic-executor-refs.dot > periodic-executor-refs.png"
67+
68+
.. image:: ../static/periodic-executor-refs.png
69+
70+
See `Stopping Executors`_ above for an explanation of the ``_EXECUTORS`` set.
71+
72+
It is a requirement of the `Server Discovery And Monitoring Spec`_ that a
73+
sleeping monitor can be awakened early. Aside from infrequent wakeups to do
74+
their appointed chores, and occasional interruptions, periodic executors also
75+
wake periodically to check if they should terminate.
76+
77+
Our first implementation of this idea was the obvious one: use the Python
78+
standard library's threading.Condition.wait with a timeout. Another thread
79+
wakes the executor early by signaling the condition variable.
80+
81+
A topology cannot signal the condition variable to tell the executor to
82+
terminate, because it would risk a deadlock in the garbage collector: no
83+
destructor or weakref callback can take a lock to signal the condition variable
84+
(see `PYTHON-863`_); thus the only way for a dying object to terminate a
85+
periodic executor is to set its "stopped" flag and let the executor see the
86+
flag next time it wakes.
87+
88+
We erred on the side of prompt cleanup, and set the check interval at 100ms. We
89+
assumed that checking a flag and going back to sleep 10 times a second was
90+
cheap on modern machines.
91+
92+
Starting in Python 3.2, the builtin C implementation of lock.acquire takes a
93+
timeout parameter, so Python 3.2+ Condition variables sleep simply by calling
94+
lock.acquire; they are implemented as efficiently as expected.
95+
96+
But in Python 2, lock.acquire has no timeout. To wait with a timeout, a Python
97+
2 condition variable sleeps a millisecond, tries to acquire the lock, sleeps
98+
twice as long, and tries again. This exponential backoff reaches a maximum
99+
sleep time of 50ms.
100+
101+
If PyMongo calls the condition variable's "wait" method with a short timeout,
102+
the exponential backoff is restarted frequently. Overall, the condition variable
103+
is not waking a few times a second, but hundreds of times. (See `PYTHON-983`_.)
104+
105+
Thus the current design of periodic executors is surprisingly simple: they
106+
do a simple `time.sleep` for a half-second, check if it is time to wake or
107+
terminate, and sleep again.
108+
109+
.. _Server Discovery And Monitoring Spec: https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring.rst#requesting-an-immediate-check
110+
111+
.. _PYTHON-863: https://jira.mongodb.org/browse/PYTHON-863
112+
113+
.. _PYTHON-983: https://jira.mongodb.org/browse/PYTHON-983

doc/static/periodic-executor-refs.dot

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ digraph "Monitor and PeriodicExecutor" {
55
monitor -> executor
66
executor -> "target()"
77
"target()" -> self_ref
8+
thread -> "target()"
89

910
// Weak references
1011
edge [style="dashed"];
3.84 KB
Loading

pymongo/periodic_executor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ def __init__(self, interval, min_interval, target, name=None):
3737
"""
3838
# threading.Event and its internal condition variable are expensive
3939
# in Python 2, see PYTHON-983. Use a boolean to know when to wake.
40+
# The executor's design is constrained by several Python issues, see
41+
# "periodic_executor.rst" in this repository.
4042
self._event = False
4143
self._interval = interval
4244
self._min_interval = min_interval

0 commit comments

Comments
 (0)