Skip to content

Commit 50eeb67

Browse files
authored
PYTHON-4348 Reinstate fork warning because network I/O and threads are not fork safe (#1597)
1 parent 70bb43b commit 50eeb67

File tree

5 files changed

+34
-9
lines changed

5 files changed

+34
-9
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ the pages will re-render and the browser will automatically refresh.
200200
- Append `test/<mod_name>.py::<class_name>::<test_name>` to run
201201
specific tests. You can omit the `<test_name>` to test a full class
202202
and the `<class_name>` to test a full module. For example:
203-
`tox -m test test/test_change_stream.py::TestUnifiedChangeStreamsErrors::test_change_stream_errors_on_ElectionInProgress`.
203+
`tox -m test -- test/test_change_stream.py::TestUnifiedChangeStreamsErrors::test_change_stream_errors_on_ElectionInProgress`.
204204
- Use the `-k` argument to select tests by pattern.
205205

206206
## Running Load Balancer Tests Locally

pymongo/mongo_client.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -879,8 +879,11 @@ def __init__(
879879
# This will be used later if we fork.
880880
MongoClient._clients[self._topology._topology_id] = self
881881

882-
def _init_background(self) -> None:
882+
def _init_background(self, old_pid: Optional[int] = None) -> None:
883883
self._topology = Topology(self._topology_settings)
884+
# Seed the topology with the old one's pid so we can detect clients
885+
# that are opened before a fork and used after.
886+
self._topology._pid = old_pid
884887

885888
def target() -> bool:
886889
client = self_ref()
@@ -903,7 +906,7 @@ def target() -> bool:
903906

904907
def _after_fork(self) -> None:
905908
"""Resets topology in a child after successfully forking."""
906-
self._init_background()
909+
self._init_background(self._topology._pid)
907910

908911
def _duplicate(self, **kwargs: Any) -> MongoClient:
909912
args = self.__init_kwargs.copy()
@@ -921,7 +924,7 @@ def _server_property(self, attr_name: str) -> Any:
921924
the server may change. In such cases, store a local reference to a
922925
ServerDescription first, then use its properties.
923926
"""
924-
server = self._topology.select_server(writable_server_selector, _Op.TEST)
927+
server = self._get_topology().select_server(writable_server_selector, _Op.TEST)
925928

926929
return getattr(server.description, attr_name)
927930

pymongo/topology.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
import os
2121
import queue
2222
import random
23+
import sys
2324
import time
2425
import warnings
2526
import weakref
27+
from pathlib import Path
2628
from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional, cast
2729

2830
from pymongo import _csot, common, helpers, periodic_executor
@@ -70,6 +72,9 @@
7072
from pymongo.typings import ClusterTime, _Address
7173

7274

75+
_pymongo_dir = str(Path(__file__).parent)
76+
77+
7378
def process_events_queue(queue_ref: weakref.ReferenceType[queue.Queue]) -> bool:
7479
q = queue_ref()
7580
if not q:
@@ -187,12 +192,17 @@ def open(self) -> None:
187192
self._pid = pid
188193
elif pid != self._pid:
189194
self._pid = pid
190-
warnings.warn(
195+
if sys.version_info[:2] >= (3, 12):
196+
kwargs = {"skip_file_prefixes": (_pymongo_dir,)}
197+
else:
198+
kwargs = {"stacklevel": 6}
199+
# Ignore B028 warning for missing stacklevel.
200+
warnings.warn( # type: ignore[call-overload] # noqa: B028
191201
"MongoClient opened before fork. May not be entirely fork-safe, "
192202
"proceed with caution. See PyMongo's documentation for details: "
193203
"https://pymongo.readthedocs.io/en/stable/faq.html#"
194204
"is-pymongo-fork-safe",
195-
stacklevel=2,
205+
**kwargs,
196206
)
197207
with self._lock:
198208
# Close servers and clear the pools.

test/test_encryption.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import textwrap
2727
import traceback
2828
import uuid
29+
import warnings
2930
from threading import Thread
3031
from typing import Any, Dict, Mapping
3132

@@ -348,7 +349,9 @@ def test_fork(self):
348349
self.addCleanup(client.close)
349350

350351
def target():
351-
client.admin.command("ping")
352+
with warnings.catch_warnings():
353+
warnings.simplefilter("ignore")
354+
client.admin.command("ping")
352355

353356
with self.fork(target):
354357
target()

test/test_fork.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import os
1919
import sys
2020
import unittest
21+
import warnings
2122
from multiprocessing import Pipe
2223

2324
sys.path[0:0] = [""]
@@ -43,7 +44,9 @@ def test_lock_client(self):
4344
with self.client._MongoClient__lock:
4445

4546
def target():
46-
self.client.admin.command("ping")
47+
with warnings.catch_warnings():
48+
warnings.simplefilter("ignore")
49+
self.client.admin.command("ping")
4750

4851
with self.fork(target):
4952
pass
@@ -72,7 +75,11 @@ def test_topology_reset(self):
7275
parent_cursor_exc = self.client._kill_cursors_executor
7376

7477
def target():
75-
self.client.admin.command("ping")
78+
# Catch the fork warning and send to the parent for assertion.
79+
with warnings.catch_warnings(record=True) as ctx:
80+
warnings.simplefilter("always")
81+
self.client.admin.command("ping")
82+
child_conn.send(str(ctx[0]))
7683
child_conn.send(self.client._topology._pid)
7784
child_conn.send(
7885
(
@@ -83,6 +90,8 @@ def target():
8390

8491
with self.fork(target):
8592
self.assertEqual(self.client._topology._pid, init_id)
93+
fork_warning = parent_conn.recv()
94+
self.assertIn("MongoClient opened before fork", fork_warning)
8695
child_id = parent_conn.recv()
8796
self.assertNotEqual(child_id, init_id)
8897
passed, msg = parent_conn.recv()

0 commit comments

Comments
 (0)