Merge branch 'master' of github.com:mongodb/mongo-python-driver

This commit is contained in:
Steven Silvester 2024-06-12 11:04:41 -05:00
commit de7d95c402
No known key found for this signature in database
GPG Key ID: B1BF5EC3A8B32F91
12 changed files with 87 additions and 85 deletions

View File

@ -378,7 +378,7 @@ class _Bulk:
if retryable and not self.started_retryable_write:
session._start_retryable_write()
self.started_retryable_write = True
await session._apply_to(cmd, retryable, ReadPreference.PRIMARY, conn)
session._apply_to(cmd, retryable, ReadPreference.PRIMARY, conn)
conn.send_cluster_time(cmd, session, client)
conn.add_server_api(cmd)
# CSOT: apply timeout before encoding the command.

View File

@ -528,7 +528,7 @@ class ClientSession:
# is in the committed state when the session is discarded.
await self._unpin()
finally:
await self._client._return_server_session(self._server_session, lock)
self._client._return_server_session(self._server_session)
self._server_session = None
def _check_ended(self) -> None:
@ -557,13 +557,13 @@ class ClientSession:
async def session_id(self) -> Mapping[str, Any]:
"""A BSON document, the opaque server session identifier."""
self._check_ended()
await self._materialize(self._client.topology_description.logical_session_timeout_minutes)
self._materialize(self._client.topology_description.logical_session_timeout_minutes)
return self._server_session.session_id
@property
async def _transaction_id(self) -> Int64:
"""The current transaction id for the underlying server session."""
await self._materialize(self._client.topology_description.logical_session_timeout_minutes)
self._materialize(self._client.topology_description.logical_session_timeout_minutes)
return self._server_session.transaction_id
@property
@ -979,16 +979,16 @@ class ClientSession:
return self._transaction.opts.read_preference
return None
async def _materialize(self, logical_session_timeout_minutes: Optional[int] = None) -> None:
def _materialize(self, logical_session_timeout_minutes: Optional[int] = None) -> None:
if isinstance(self._server_session, _EmptyServerSession):
old = self._server_session
self._server_session = await self._client._topology.get_server_session(
self._server_session = self._client._topology.get_server_session(
logical_session_timeout_minutes
)
if old.started_retryable_write:
self._server_session.inc_transaction_id()
async def _apply_to(
def _apply_to(
self,
command: MutableMapping[str, Any],
is_retryable: bool,
@ -1000,7 +1000,7 @@ class ClientSession:
raise ConfigurationError("Sessions are not supported by this MongoDB deployment")
return
self._check_ended()
await self._materialize(conn.logical_session_timeout_minutes)
self._materialize(conn.logical_session_timeout_minutes)
if self.options.snapshot:
self._update_read_concern(command, conn)
@ -1103,7 +1103,7 @@ class _ServerSession:
class _ServerSessionPool(collections.deque):
"""Pool of _ServerSession objects.
This class is not thread-safe, access it while holding the Topology lock.
This class is thread-safe.
"""
def __init__(self, *args: Any, **kwargs: Any):
@ -1116,8 +1116,11 @@ class _ServerSessionPool(collections.deque):
def pop_all(self) -> list[_ServerSession]:
ids = []
while self:
ids.append(self.pop().session_id)
while True:
try:
ids.append(self.pop().session_id)
except IndexError:
break
return ids
def get_server_session(self, session_timeout_minutes: Optional[int]) -> _ServerSession:
@ -1129,23 +1132,17 @@ class _ServerSessionPool(collections.deque):
self._clear_stale(session_timeout_minutes)
# The most recently used sessions are on the left.
while self:
s = self.popleft()
while True:
try:
s = self.popleft()
except IndexError:
break
if not s.timed_out(session_timeout_minutes):
return s
return _ServerSession(self.generation)
def return_server_session(
self, server_session: _ServerSession, session_timeout_minutes: Optional[int]
) -> None:
if session_timeout_minutes is not None:
self._clear_stale(session_timeout_minutes)
if server_session.timed_out(session_timeout_minutes):
return
self.return_server_session_no_lock(server_session)
def return_server_session_no_lock(self, server_session: _ServerSession) -> None:
def return_server_session(self, server_session: _ServerSession) -> None:
# Discard sessions from an old pool to avoid duplicate sessions in the
# child process after a fork.
if server_session.generation == self.generation and not server_session.dirty:
@ -1153,9 +1150,12 @@ class _ServerSessionPool(collections.deque):
def _clear_stale(self, session_timeout_minutes: Optional[int]) -> None:
# Clear stale sessions. The least recently used are on the right.
while self:
if self[-1].timed_out(session_timeout_minutes):
self.pop()
else:
while True:
try:
s = self.pop()
except IndexError:
break
if not s.timed_out(session_timeout_minutes):
self.append(s)
# The remaining sessions also haven't timed out.
break

View File

@ -15,6 +15,7 @@
"""Collection level utilities for Mongo."""
from __future__ import annotations
import warnings
from collections import abc
from typing import (
TYPE_CHECKING,
@ -248,6 +249,11 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
if create or kwargs:
if _IS_SYNC:
warnings.warn(
"The `create` and `kwargs` arguments to Collection are deprecated and will be removed in PyMongo 5.0",
DeprecationWarning,
stacklevel=2,
)
self._create(kwargs, session) # type: ignore[unused-coroutine]
else:
raise ValueError(

View File

@ -394,7 +394,7 @@ class _Query:
session = self.session
conn.add_server_api(cmd)
if session:
await session._apply_to(cmd, False, self.read_preference, conn)
session._apply_to(cmd, False, self.read_preference, conn)
# Explain does not support readConcern.
if not explain and not session.in_transaction:
session._update_read_concern(cmd, conn)
@ -546,7 +546,7 @@ class _GetMore:
conn,
)
if self.session:
await self.session._apply_to(cmd, False, self.read_preference, conn)
self.session._apply_to(cmd, False, self.read_preference, conn)
conn.add_server_api(cmd)
conn.send_cluster_time(cmd, self.session, self.client)
# Support auto encryption

View File

@ -902,6 +902,8 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
def _after_fork(self) -> None:
"""Resets topology in a child after successfully forking."""
self._init_background()
# Reset the session pool to avoid duplicate sessions in the child process.
self._topology._session_pool.reset()
def _duplicate(self, **kwargs: Any) -> AsyncMongoClient:
args = self._init_kwargs.copy()
@ -1508,7 +1510,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
.. versionchanged:: 3.6
End all server sessions created by this client.
"""
session_ids = await self._topology.pop_all_sessions()
session_ids = self._topology.pop_all_sessions()
if session_ids:
await self._end_sessions(session_ids)
# Stop the periodic task thread and then send pending killCursor
@ -2006,13 +2008,13 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
else:
helpers._handle_exception()
async def _return_server_session(
self, server_session: Union[_ServerSession, _EmptyServerSession], lock: bool
def _return_server_session(
self, server_session: Union[_ServerSession, _EmptyServerSession]
) -> None:
"""Internal: return a _ServerSession to the pool."""
if isinstance(server_session, _EmptyServerSession):
return None
return await self._topology.return_server_session(server_session, lock)
return self._topology.return_server_session(server_session)
@contextlib.asynccontextmanager
async def _tmp_session(

View File

@ -982,7 +982,7 @@ class Connection:
self.add_server_api(spec)
if session:
await session._apply_to(spec, retryable_write, read_preference, self)
session._apply_to(spec, retryable_write, read_preference, self)
self.send_cluster_time(spec, session, client)
listeners = self.listeners if publish_events else None
unacknowledged = bool(write_concern and not write_concern.acknowledged)

View File

@ -671,25 +671,16 @@ class Topology:
def description(self) -> TopologyDescription:
return self._description
async def pop_all_sessions(self) -> list[_ServerSession]:
def pop_all_sessions(self) -> list[_ServerSession]:
"""Pop all session ids from the pool."""
async with self._lock:
return self._session_pool.pop_all()
return self._session_pool.pop_all()
async def get_server_session(self, session_timeout_minutes: Optional[int]) -> _ServerSession:
def get_server_session(self, session_timeout_minutes: Optional[int]) -> _ServerSession:
"""Start or resume a server session, or raise ConfigurationError."""
async with self._lock:
return self._session_pool.get_server_session(session_timeout_minutes)
return self._session_pool.get_server_session(session_timeout_minutes)
async def return_server_session(self, server_session: _ServerSession, lock: bool) -> None:
if lock:
async with self._lock:
self._session_pool.return_server_session(
server_session, self._description.logical_session_timeout_minutes
)
else:
# Called from a __del__ method, can't use a lock.
self._session_pool.return_server_session_no_lock(server_session)
def return_server_session(self, server_session: _ServerSession) -> None:
self._session_pool.return_server_session(server_session)
def _new_selection(self) -> Selection:
"""A Selection object, initially including all known servers.

View File

@ -528,7 +528,7 @@ class ClientSession:
# is in the committed state when the session is discarded.
self._unpin()
finally:
self._client._return_server_session(self._server_session, lock)
self._client._return_server_session(self._server_session)
self._server_session = None
def _check_ended(self) -> None:
@ -1099,7 +1099,7 @@ class _ServerSession:
class _ServerSessionPool(collections.deque):
"""Pool of _ServerSession objects.
This class is not thread-safe, access it while holding the Topology lock.
This class is thread-safe.
"""
def __init__(self, *args: Any, **kwargs: Any):
@ -1112,8 +1112,11 @@ class _ServerSessionPool(collections.deque):
def pop_all(self) -> list[_ServerSession]:
ids = []
while self:
ids.append(self.pop().session_id)
while True:
try:
ids.append(self.pop().session_id)
except IndexError:
break
return ids
def get_server_session(self, session_timeout_minutes: Optional[int]) -> _ServerSession:
@ -1125,23 +1128,17 @@ class _ServerSessionPool(collections.deque):
self._clear_stale(session_timeout_minutes)
# The most recently used sessions are on the left.
while self:
s = self.popleft()
while True:
try:
s = self.popleft()
except IndexError:
break
if not s.timed_out(session_timeout_minutes):
return s
return _ServerSession(self.generation)
def return_server_session(
self, server_session: _ServerSession, session_timeout_minutes: Optional[int]
) -> None:
if session_timeout_minutes is not None:
self._clear_stale(session_timeout_minutes)
if server_session.timed_out(session_timeout_minutes):
return
self.return_server_session_no_lock(server_session)
def return_server_session_no_lock(self, server_session: _ServerSession) -> None:
def return_server_session(self, server_session: _ServerSession) -> None:
# Discard sessions from an old pool to avoid duplicate sessions in the
# child process after a fork.
if server_session.generation == self.generation and not server_session.dirty:
@ -1149,9 +1146,12 @@ class _ServerSessionPool(collections.deque):
def _clear_stale(self, session_timeout_minutes: Optional[int]) -> None:
# Clear stale sessions. The least recently used are on the right.
while self:
if self[-1].timed_out(session_timeout_minutes):
self.pop()
else:
while True:
try:
s = self.pop()
except IndexError:
break
if not s.timed_out(session_timeout_minutes):
self.append(s)
# The remaining sessions also haven't timed out.
break

View File

@ -15,6 +15,7 @@
"""Collection level utilities for Mongo."""
from __future__ import annotations
import warnings
from collections import abc
from typing import (
TYPE_CHECKING,
@ -251,6 +252,11 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
if create or kwargs:
if _IS_SYNC:
warnings.warn(
"The `create` and `kwargs` arguments to Collection are deprecated and will be removed in PyMongo 5.0",
DeprecationWarning,
stacklevel=2,
)
self._create(kwargs, session) # type: ignore[unused-coroutine]
else:
raise ValueError("Collection does not support the `create` or `kwargs` arguments.")

View File

@ -901,6 +901,8 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
def _after_fork(self) -> None:
"""Resets topology in a child after successfully forking."""
self._init_background()
# Reset the session pool to avoid duplicate sessions in the child process.
self._topology._session_pool.reset()
def _duplicate(self, **kwargs: Any) -> MongoClient:
args = self._init_kwargs.copy()
@ -2004,12 +2006,12 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
helpers._handle_exception()
def _return_server_session(
self, server_session: Union[_ServerSession, _EmptyServerSession], lock: bool
self, server_session: Union[_ServerSession, _EmptyServerSession]
) -> None:
"""Internal: return a _ServerSession to the pool."""
if isinstance(server_session, _EmptyServerSession):
return None
return self._topology.return_server_session(server_session, lock)
return self._topology.return_server_session(server_session)
@contextlib.contextmanager
def _tmp_session(

View File

@ -671,23 +671,14 @@ class Topology:
def pop_all_sessions(self) -> list[_ServerSession]:
"""Pop all session ids from the pool."""
with self._lock:
return self._session_pool.pop_all()
return self._session_pool.pop_all()
def get_server_session(self, session_timeout_minutes: Optional[int]) -> _ServerSession:
"""Start or resume a server session, or raise ConfigurationError."""
with self._lock:
return self._session_pool.get_server_session(session_timeout_minutes)
return self._session_pool.get_server_session(session_timeout_minutes)
def return_server_session(self, server_session: _ServerSession, lock: bool) -> None:
if lock:
with self._lock:
self._session_pool.return_server_session(
server_session, self._description.logical_session_timeout_minutes
)
else:
# Called from a __del__ method, can't use a lock.
self._session_pool.return_server_session_no_lock(server_session)
def return_server_session(self, server_session: _ServerSession) -> None:
self._session_pool.return_server_session(server_session)
def _new_selection(self) -> Selection:
"""A Selection object, initially including all known servers.

View File

@ -198,7 +198,11 @@ class TestCollection(IntegrationTest):
"create create_test_no_wc collection",
)
db.create_test_no_wc.drop()
Collection(db, name="create_test_no_wc", create=True)
with self.assertWarns(
DeprecationWarning,
msg="The `create` and `kwargs` arguments to Collection are deprecated and will be removed in PyMongo 5.0",
):
Collection(db, name="create_test_no_wc", create=True)
wait_until(
lambda: "create_test_no_wc" in db.list_collection_names(),
"create create_test_no_wc collection",