Skip to content

Commit e554d61

Browse files
committed
PYTHON-1419 Call endSessions on MongoClient.close.
1 parent 8416c73 commit e554d61

File tree

5 files changed

+80
-4
lines changed

5 files changed

+80
-4
lines changed

pymongo/client_session.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,12 @@ class _ServerSessionPool(collections.deque):
236236
237237
This class is not thread-safe, access it while holding the Topology lock.
238238
"""
239+
def pop_all(self):
240+
ids = []
241+
while self:
242+
ids.append(self.pop().session_id)
243+
return ids
244+
239245
def get_server_session(self, session_timeout_minutes):
240246
# Although the Driver Sessions Spec says we only clear stale sessions
241247
# in return_server_session, PyMongo can't take a lock when returning

pymongo/common.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@
9494
# Error codes to ignore if GridFS calls createIndex on a secondary
9595
UNAUTHORIZED_CODES = (13, 16547, 16548)
9696

97+
# Maximum number of sessions to send in a single endSessions command.
98+
# From the driver sessions spec.
99+
_MAX_END_SESSIONS = 10000
100+
97101

98102
def partition_node(node):
99103
"""Split a host:port string into (host, int(port)) pair."""

pymongo/mongo_client.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -857,13 +857,42 @@ def _is_writable(self):
857857
except ConnectionFailure:
858858
return False
859859

860+
def _end_sessions(self, session_ids):
861+
"""Send endSessions command(s) with the given session ids."""
862+
try:
863+
# Use SocketInfo.command directly to avoid implicitly creating
864+
# another session.
865+
with self._socket_for_reads(
866+
ReadPreference.PRIMARY_PREFERRED) as (sock_info, slave_ok):
867+
if not sock_info.supports_sessions:
868+
return
869+
870+
for i in range(0, len(session_ids), common._MAX_END_SESSIONS):
871+
spec = SON([('endSessions',
872+
session_ids[i:i + common._MAX_END_SESSIONS])])
873+
sock_info.command(
874+
'admin', spec, slave_ok=slave_ok, client=self)
875+
except PyMongoError:
876+
# Drivers MUST ignore any errors returned by the endSessions
877+
# command.
878+
pass
879+
860880
def close(self):
861-
"""Disconnect from MongoDB.
881+
"""Cleanup client resources and disconnect from MongoDB.
882+
883+
On MongoDB >= 3.6, end all server sessions created by this client by
884+
sending one or more endSessions commands.
862885
863886
Close all sockets in the connection pools and stop the monitor threads.
864887
If this instance is used again it will be automatically re-opened and
865888
the threads restarted.
889+
890+
.. versionchanged:: 3.6
891+
End all server sessions created by this client.
866892
"""
893+
session_ids = self._topology.pop_all_sessions()
894+
if session_ids:
895+
self._end_sessions(session_ids)
867896
# Run _process_periodic_tasks to send pending killCursor requests
868897
# before closing the topology.
869898
self._process_periodic_tasks()
@@ -1307,7 +1336,7 @@ def _process_periodic_tasks(self):
13071336
except Exception:
13081337
helpers._handle_exception()
13091338

1310-
def start_session(self, **kwargs):
1339+
def start_session(self, causal_consistency=True):
13111340
"""Start a logical session.
13121341
13131342
This method takes the same parameters as
@@ -1318,6 +1347,12 @@ def start_session(self, **kwargs):
13181347
if this client has been authenticated to multiple databases using the
13191348
deprecated method :meth:`~pymongo.database.Database.authenticate`.
13201349
1350+
A :class:`~pymongo.client_session.ClientSession` may only be used with
1351+
the MongoClient that started it.
1352+
1353+
:Returns:
1354+
An instance of :class:`~pymongo.client_session.ClientSession`.
1355+
13211356
.. versionadded:: 3.6
13221357
"""
13231358
# Driver Sessions Spec: "If startSession is called when multiple users
@@ -1330,8 +1365,10 @@ def start_session(self, **kwargs):
13301365

13311366
# Raises ConfigurationError if sessions are not supported.
13321367
server_session = self._get_server_session()
1333-
opts = client_session.SessionOptions(**kwargs)
1334-
return client_session.ClientSession(self, server_session, opts, authset)
1368+
opts = client_session.SessionOptions(
1369+
causal_consistency=causal_consistency)
1370+
return client_session.ClientSession(
1371+
self, server_session, opts, authset)
13351372

13361373
def _get_server_session(self):
13371374
"""Internal: start or resume a _ServerSession."""

pymongo/topology.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,11 @@ def close(self):
397397
def description(self):
398398
return self._description
399399

400+
def pop_all_sessions(self):
401+
"""Pop all session ids from the pool."""
402+
with self._lock:
403+
return self._session_pool.pop_all()
404+
400405
def get_server_session(self):
401406
"""Start or resume a server session, or raise ConfigurationError."""
402407
with self._lock:

test/test_session.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from bson.py3compat import StringIO
2222
from gridfs import GridFS, GridFSBucket
2323
from pymongo import ASCENDING, InsertOne, IndexModel, OFF, monitoring
24+
from pymongo.common import _MAX_END_SESSIONS
2425
from pymongo.errors import (ConfigurationError,
2526
InvalidOperation,
2627
OperationFailure)
@@ -943,6 +944,29 @@ def test_cluster_time_no_server_support(self):
943944
'$clusterTime')
944945
self.assertIsNone(after_cluster_time)
945946

947+
def test_end_sessions(self):
948+
listener = SessionTestListener()
949+
client = rs_or_single_client(event_listeners=[listener])
950+
# Start many sessions.
951+
sessions = [client.start_session()
952+
for _ in range(_MAX_END_SESSIONS + 1)]
953+
for s in sessions:
954+
s.end_session()
955+
956+
# Closing the client should end all sessions and clear the pool.
957+
self.assertEqual(len(client._topology._session_pool),
958+
_MAX_END_SESSIONS + 1)
959+
client.close()
960+
self.assertEqual(len(client._topology._session_pool), 0)
961+
end_sessions = [e for e in listener.results['started']
962+
if e.command_name == 'endSessions']
963+
self.assertEqual(len(end_sessions), 2)
964+
965+
# Closing again should not send any commands.
966+
listener.results.clear()
967+
client.close()
968+
self.assertEqual(len(listener.results['started']), 0)
969+
946970

947971
class TestSessionsMultiAuth(IntegrationTest):
948972
@client_context.require_auth

0 commit comments

Comments
 (0)