Skip to content

Commit a801c38

Browse files
committed
PYTHON-525 Restart monitor threads after fork.
A reimplementation of PYTHON-549 for PyMongo 3's new MongoClient.
1 parent fd3154d commit a801c38

7 files changed

Lines changed: 71 additions & 89 deletions

File tree

pymongo/cluster.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import threading
1919
import time
2020

21+
from bson.py3compat import itervalues
2122
from pymongo import common
2223
from pymongo.cluster_description import (updated_cluster_description,
2324
CLUSTER_TYPE,
@@ -44,7 +45,7 @@ def __init__(self, cluster_settings):
4445
self._servers = {}
4546

4647
def open(self):
47-
"""Start monitoring.
48+
"""Start monitoring, or restart after a fork.
4849
4950
No effect if called multiple times.
5051
"""
@@ -204,10 +205,17 @@ def description(self):
204205
return self._description
205206

206207
def _ensure_opened(self):
207-
"""Start monitors. Hold the lock when calling this."""
208+
"""Start monitors, or restart after a fork.
209+
210+
Hold the lock when calling this.
211+
"""
208212
if not self._opened:
209213
self._opened = True
210214
self._update_servers()
215+
else:
216+
# Restart monitors if we forked since previous call.
217+
for server in itervalues(self._servers):
218+
server.open()
211219

212220
def _request_check_all(self):
213221
"""Wake all monitors. Hold the lock when calling this."""

pymongo/monitor.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from pymongo.server_description import ServerDescription
2828

2929

30-
class Monitor(threading.Thread):
30+
class Monitor(object):
3131
def __init__(
3232
self,
3333
server_description,
@@ -43,13 +43,31 @@ def __init__(
4343
Monitor.
4444
"""
4545
super(Monitor, self).__init__()
46-
self.daemon = True # Python 2.6's way to do setDaemon(True).
4746
self._server_description = server_description
4847
self._cluster = weakref.proxy(cluster)
4948
self._pool = pool
5049
self._settings = cluster_settings
5150
self._stopped = False
5251
self._event = thread_util.Event(self._settings.condition_class)
52+
self._thread = None
53+
54+
def open(self):
55+
"""Start monitoring, or restart after a fork.
56+
57+
Multiple calls have no effect.
58+
"""
59+
started = False
60+
try:
61+
started = self._thread and self._thread.is_alive()
62+
except ReferenceError:
63+
# Thread terminated.
64+
pass
65+
66+
if not started:
67+
thread = threading.Thread(target=self.run)
68+
thread.daemon = True
69+
self._thread = weakref.proxy(thread)
70+
thread.start()
5371

5472
def close(self):
5573
"""Disconnect and stop monitoring.
@@ -62,6 +80,14 @@ def close(self):
6280
# Awake the thread so it notices that _stopped is True.
6381
self.request_check()
6482

83+
def join(self, timeout=None):
84+
if self._thread is not None:
85+
try:
86+
self._thread.join(timeout)
87+
except ReferenceError:
88+
# Thread already terminated.
89+
pass
90+
6591
def request_check(self):
6692
"""If the monitor is sleeping, wake and check the server soon."""
6793
self._event.set()

pymongo/server.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ def __init__(self, server_description, pool, monitor):
2929
self._monitor = monitor
3030

3131
def open(self):
32-
self._monitor.start()
32+
"""Start monitoring, or restart after a fork.
33+
34+
Multiple calls have no effect.
35+
"""
36+
self._monitor.open()
3337

3438
def close(self):
3539
self._monitor.close()

test/test_client.py

Lines changed: 26 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616

1717
import contextlib
1818
import datetime
19+
import multiprocessing
1920
import os
2021
import threading
2122
import socket
2223
import sys
2324
import time
25+
import traceback
2426
import warnings
2527

2628
sys.path[0:0] = [""]
@@ -38,7 +40,8 @@
3840
InvalidName,
3941
OperationFailure,
4042
CursorNotFound)
41-
from pymongo.server_selectors import writable_server_selector
43+
from pymongo.server_selectors import (any_server_selector,
44+
writable_server_selector)
4245
from pymongo.server_type import SERVER_TYPE
4346
from test import (client_context,
4447
client_knobs,
@@ -442,56 +445,38 @@ def test_fork(self):
442445
if sys.platform == "win32":
443446
raise SkipTest("Can't fork on windows")
444447

445-
try:
446-
from multiprocessing import Process, Pipe
447-
except ImportError:
448-
raise SkipTest("No multiprocessing module")
449-
450448
db = self.client.pymongo_test
451449

452-
# Failure occurs if the client is used before the fork
450+
# Ensure a socket is opened before the fork.
453451
db.test.find_one()
454-
db.connection.end_request()
455-
456-
def loop(pipe):
457-
while True:
458-
try:
459-
db.test.insert({"a": "b"})
460-
for _ in db.test.find():
461-
pass
462-
except:
463-
pipe.send(True)
464-
os._exit(1)
465452

466-
cp1, cc1 = Pipe()
467-
cp2, cc2 = Pipe()
468-
469-
p1 = Process(target=loop, args=(cc1,))
470-
p2 = Process(target=loop, args=(cc2,))
471-
472-
p1.start()
473-
p2.start()
453+
def f(pipe):
454+
try:
455+
servers = self.client._cluster.select_servers(
456+
any_server_selector)
474457

475-
p1.join(1)
476-
p2.join(1)
458+
# In child, only the thread that called fork() is alive.
459+
assert not any(s._monitor._thread.is_alive()
460+
for s in servers)
477461

478-
p1.terminate()
479-
p2.terminate()
462+
db.test.find_one()
480463

481-
p1.join()
482-
p2.join()
464+
wait_until(
465+
lambda: all(s._monitor._thread.is_alive() for s in servers),
466+
"restart monitor threads")
467+
except:
468+
traceback.print_exc() # Aid debugging.
469+
pipe.send(True)
483470

484-
cc1.close()
485-
cc2.close()
471+
parent_pipe, child_pipe = multiprocessing.Pipe()
472+
p = multiprocessing.Process(target=f, args=(child_pipe,))
473+
p.start()
474+
p.join(10)
475+
child_pipe.close()
486476

487-
# recv will only have data if the subprocess failed
488-
try:
489-
cp1.recv()
490-
self.fail()
491-
except EOFError:
492-
pass
477+
# Pipe will only have data if the child process failed.
493478
try:
494-
cp2.recv()
479+
parent_pipe.recv()
495480
self.fail()
496481
except EOFError:
497482
pass

test/test_cluster.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def __init__(self, server_description, cluster, pool, cluster_settings):
7070
self._server_description = server_description
7171
self._cluster = cluster
7272

73-
def start(self):
73+
def open(self):
7474
pass
7575

7676
def request_check(self):

test/test_cluster_spec.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def __init__(self, server_description, cluster, pool, cluster_settings):
6969
self._server_description = server_description
7070
self._cluster = cluster
7171

72-
def start(self):
72+
def open(self):
7373
pass
7474

7575
def request_check(self):

test/test_replica_set_client.py

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -520,47 +520,6 @@ def close():
520520
self.assertRaises(InvalidOperation, c.db.collection.find_one)
521521
self.assertRaises(InvalidOperation, c.db.collection.insert, {})
522522

523-
def test_fork(self):
524-
# After a fork the monitor thread is gone.
525-
# Verify that schedule_refresh restarts it.
526-
if sys.platform == "win32":
527-
raise SkipTest("Can't fork on Windows")
528-
529-
try:
530-
from multiprocessing import Process, Pipe
531-
except ImportError:
532-
raise SkipTest("No multiprocessing module")
533-
534-
client = client_context.rs_client
535-
536-
def f(pipe):
537-
try:
538-
# Trigger a refresh.
539-
self.assertFalse(
540-
client._MongoReplicaSetClient__monitor.isAlive())
541-
542-
client.disconnect()
543-
self.assertSoon(
544-
lambda: client._MongoReplicaSetClient__monitor.isAlive())
545-
546-
client.db.collection.find_one() # No error.
547-
except:
548-
traceback.print_exc()
549-
pipe.send(True)
550-
551-
cp, cc = Pipe()
552-
p = Process(target=f, args=(cc,))
553-
p.start()
554-
p.join(10)
555-
cc.close()
556-
557-
# recv will only have data if the subprocess failed
558-
try:
559-
cp.recv()
560-
self.fail()
561-
except EOFError:
562-
pass
563-
564523
def test_document_class(self):
565524
c = client_context.rs_client
566525
db = c.pymongo_test

0 commit comments

Comments
 (0)