Skip to content

Commit 001e62b

Browse files
committed
Merge branch 'master' of github.com:MongoEngine/mongoengine into remove_deprecated_warnings
2 parents 829b303 + 0dcb09f commit 001e62b

19 files changed

+646
-79
lines changed

.github/workflows/github-actions.yml

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,16 @@ env:
2020
MONGODB_6_0: "6.0"
2121
MONGODB_7_0: "7.0"
2222

23-
PYMONGO_3_4: 3.4
24-
PYMONGO_3_6: 3.6
25-
PYMONGO_3_9: 3.9
2623
PYMONGO_3_11: 3.11
2724
PYMONGO_3_12: 3.12
25+
PYMONGO_3_13: 3.13
2826
PYMONGO_4_0: 4.0
2927
PYMONGO_4_3: 4.3.3
3028
PYMONGO_4_4: 4.4.1
3129
PYMONGO_4_6: 4.6.2
3230
PYMONGO_4_7: 4.7.3
3331
PYMONGO_4_8: 4.8.0
32+
PYMONGO_4_9: 4.9
3433

3534
MAIN_PYTHON_VERSION: 3.9
3635

@@ -61,15 +60,12 @@ jobs:
6160
MONGODB: [$MONGODB_4_0]
6261
PYMONGO: [$PYMONGO_3_11]
6362
include:
64-
- python-version: 3.7
65-
MONGODB: $MONGODB_3_6
66-
PYMONGO: $PYMONGO_3_9
6763
- python-version: 3.8
68-
MONGODB: $MONGODB_4_4
69-
PYMONGO: $PYMONGO_3_11
64+
MONGODB: $MONGODB_3_6
65+
PYMONGO: $PYMONGO_3_12
7066
- python-version: 3.9
7167
MONGODB: $MONGODB_4_4
72-
PYMONGO: $PYMONGO_3_12
68+
PYMONGO: $PYMONGO_3_13
7369
- python-version: "3.10"
7470
MONGODB: $MONGODB_4_4
7571
PYMONGO: $PYMONGO_4_0
@@ -88,6 +84,9 @@ jobs:
8884
- python-version: "3.11"
8985
MONGODB: $MONGODB_7_0
9086
PYMONGO: $PYMONGO_4_8
87+
- python-version: "3.11"
88+
MONGODB: $MONGODB_7_0
89+
PYMONGO: $PYMONGO_4_9
9190
steps:
9291
- uses: actions/checkout@v4
9392
- name: Set up Python ${{ matrix.python-version }}

.github/workflows/start_mongo.sh

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,18 @@ MONGODB=$1
55
mongodb_dir=$(find ${PWD}/ -type d -name "mongodb-linux-x86_64*")
66

77
mkdir $mongodb_dir/data
8-
$mongodb_dir/bin/mongod --dbpath $mongodb_dir/data --logpath $mongodb_dir/mongodb.log --fork
8+
9+
args=(--dbpath $mongodb_dir/data --logpath $mongodb_dir/mongodb.log --fork --replSet mongoengine)
10+
if (( $(echo "$MONGODB > 3.8" | bc -l) )); then
11+
args+=(--setParameter maxTransactionLockRequestTimeoutMillis=1000)
12+
fi
13+
14+
$mongodb_dir/bin/mongod "${args[@]}"
915

1016
if (( $(echo "$MONGODB < 6.0" | bc -l) )); then
11-
mongo --quiet --eval 'db.runCommand("ping").ok' # Make sure mongo is awake
17+
mongo --verbose --eval "rs.initiate()"
18+
mongo --quiet --eval "rs.status().ok"
1219
else
13-
mongosh --quiet --eval 'db.runCommand("ping").ok' # Make sure mongo is awake
20+
mongosh --verbose --eval "rs.initiate()"
21+
mongosh --quiet --eval "rs.status().ok"
1422
fi

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,4 @@ that much better:
265265
* Ido Shraga (https://github.com/idoshr)
266266
* Terence Honles (https://github.com/terencehonles)
267267
* Sean Bermejo (https://github.com/seanbermejo)
268+
* Juan Gutierrez (https://github.com/juannyg)

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM mongo:5
1+
FROM mongo:4.0
22

33
COPY ./entrypoint.sh entrypoint.sh
44
RUN chmod u+x entrypoint.sh

docs/changelog.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ Changelog
77
Development
88
===========
99
- (Fill this out as you fix issues and develop your features).
10+
- Add support for transaction through run_in_transaction (kudos to juannyG for this) #2569
11+
Some considerations:
12+
- make sure to read https://www.mongodb.com/docs/manual/core/transactions-in-applications/#callback-api-vs-core-api
13+
- run_in_transaction context manager relies on Pymongo coreAPI, it will retry automatically in case of `UnknownTransactionCommitResult` but not `TransientTransactionError` exceptions
14+
- Using .count() in a transaction will always use Collection.count_document (as estimated_document_count is not supported in transactions)
1015
- Further to the deprecation warning, remove ability to use an unpacked list to `Queryset.aggregate(*pipeline)`, a plain list must be provided instead `Queryset.aggregate(pipeline)`, as it's closer to pymongo interface
1116
- Further to the deprecation warning, remove `full_response` from `QuerySet.modify` as it wasn't supported with Pymongo 3+
1217
- Fixed stacklevel of many warnings (to point places emitting the warning more accurately)

entrypoint.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
mongod --replSet mongoengine --fork --logpath=/var/log/mongodb.log
44
mongo db --eval "rs.initiate()"
55
mongod --shutdown
6-
mongod --replSet mongoengine --bind_ip 0.0.0.0
6+
mongod --replSet mongoengine --bind_ip 0.0.0.0 --setParameter maxTransactionLockRequestTimeoutMillis=1000

mongoengine/connection.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
import collections
2+
import threading
13
import warnings
24

35
from pymongo import MongoClient, ReadPreference, uri_parser
46
from pymongo.common import _UUID_REPRESENTATIONS
5-
from pymongo.database import _check_name
7+
8+
try:
9+
from pymongo.database_shared import _check_name
10+
except ImportError:
11+
from pymongo.database import _check_name
612

713
# DriverInfo was added in PyMongo 3.7.
814
try:
@@ -35,6 +41,7 @@
3541
_connections = {}
3642
_dbs = {}
3743

44+
3845
READ_PREFERENCE = ReadPreference.PRIMARY
3946

4047

@@ -471,3 +478,37 @@ def connect(db=None, alias=DEFAULT_CONNECTION_NAME, **kwargs):
471478
# Support old naming convention
472479
_get_connection = get_connection
473480
_get_db = get_db
481+
482+
483+
class _LocalSessions(threading.local):
484+
def __init__(self):
485+
self.sessions = collections.deque()
486+
487+
def append(self, session):
488+
self.sessions.append(session)
489+
490+
def get_current(self):
491+
if len(self.sessions):
492+
return self.sessions[-1]
493+
494+
def clear_current(self):
495+
if len(self.sessions):
496+
self.sessions.pop()
497+
498+
def clear_all(self):
499+
self.sessions.clear()
500+
501+
502+
_local_sessions = _LocalSessions()
503+
504+
505+
def _set_session(session):
506+
_local_sessions.append(session)
507+
508+
509+
def _get_session():
510+
return _local_sessions.get_current()
511+
512+
513+
def _clear_session():
514+
return _local_sessions.clear_current()

mongoengine/context_managers.py

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
import contextlib
2+
import logging
23
import threading
34
from contextlib import contextmanager
45

6+
from pymongo.errors import ConnectionFailure, OperationFailure
57
from pymongo.read_concern import ReadConcern
68
from pymongo.write_concern import WriteConcern
79

810
from mongoengine.base.fields import _no_dereference_for_fields
911
from mongoengine.common import _import_class
10-
from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
12+
from mongoengine.connection import (
13+
DEFAULT_CONNECTION_NAME,
14+
_clear_session,
15+
_get_session,
16+
_set_session,
17+
get_connection,
18+
get_db,
19+
)
1120
from mongoengine.pymongo_support import count_documents
1221

1322
__all__ = (
@@ -19,6 +28,7 @@
1928
"set_write_concern",
2029
"set_read_write_concern",
2130
"no_dereferencing_active_for_class",
31+
"run_in_transaction",
2232
)
2333

2434

@@ -231,11 +241,11 @@ def __init__(self, alias=DEFAULT_CONNECTION_NAME):
231241
}
232242

233243
def _turn_on_profiling(self):
234-
profile_update_res = self.db.command({"profile": 0})
244+
profile_update_res = self.db.command({"profile": 0}, session=_get_session())
235245
self.initial_profiling_level = profile_update_res["was"]
236246

237247
self.db.system.profile.drop()
238-
self.db.command({"profile": 2})
248+
self.db.command({"profile": 2}, session=_get_session())
239249

240250
def _resets_profiling(self):
241251
self.db.command({"profile": self.initial_profiling_level})
@@ -311,3 +321,60 @@ def set_read_write_concern(collection, write_concerns, read_concerns):
311321
write_concern=WriteConcern(**combined_write_concerns),
312322
read_concern=ReadConcern(**combined_read_concerns),
313323
)
324+
325+
326+
def _commit_with_retry(session):
327+
while True:
328+
try:
329+
# Commit uses write concern set at transaction start.
330+
session.commit_transaction()
331+
break
332+
except (ConnectionFailure, OperationFailure) as exc:
333+
# Can retry commit
334+
if exc.has_error_label("UnknownTransactionCommitResult"):
335+
logging.warning(
336+
"UnknownTransactionCommitResult, retrying commit operation ..."
337+
)
338+
continue
339+
else:
340+
# Error during commit
341+
raise
342+
343+
344+
@contextmanager
345+
def run_in_transaction(
346+
alias=DEFAULT_CONNECTION_NAME, session_kwargs=None, transaction_kwargs=None
347+
):
348+
"""run_in_transaction context manager
349+
Execute queries within the context in a database transaction.
350+
351+
Usage:
352+
353+
.. code-block:: python
354+
355+
class A(Document):
356+
name = StringField()
357+
358+
with run_in_transaction():
359+
a_doc = A.objects.create(name="a")
360+
a_doc.update(name="b")
361+
362+
Be aware that:
363+
- Mongo transactions run inside a session which is bound to a connection. If you attempt to
364+
execute a transaction across a different connection alias, pymongo will raise an exception. In
365+
other words: you cannot create a transaction that crosses different database connections. That
366+
said, multiple transaction can be nested within the same session for particular connection.
367+
368+
For more information regarding pymongo transactions: https://pymongo.readthedocs.io/en/stable/api/pymongo/client_session.html#transactions
369+
"""
370+
conn = get_connection(alias)
371+
session_kwargs = session_kwargs or {}
372+
with conn.start_session(**session_kwargs) as session:
373+
transaction_kwargs = transaction_kwargs or {}
374+
with session.start_transaction(**transaction_kwargs):
375+
try:
376+
_set_session(session)
377+
yield
378+
_commit_with_retry(session)
379+
finally:
380+
_clear_session()

mongoengine/dereference.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
get_document,
99
)
1010
from mongoengine.base.datastructures import LazyReference
11-
from mongoengine.connection import get_db
11+
from mongoengine.connection import _get_session, get_db
1212
from mongoengine.document import Document, EmbeddedDocument
1313
from mongoengine.fields import (
1414
DictField,
@@ -187,13 +187,15 @@ def _fetch_objects(self, doc_type=None):
187187

188188
if doc_type:
189189
references = doc_type._get_db()[collection].find(
190-
{"_id": {"$in": refs}}
190+
{"_id": {"$in": refs}}, session=_get_session()
191191
)
192192
for ref in references:
193193
doc = doc_type._from_son(ref)
194194
object_map[(collection, doc.id)] = doc
195195
else:
196-
references = get_db()[collection].find({"_id": {"$in": refs}})
196+
references = get_db()[collection].find(
197+
{"_id": {"$in": refs}}, session=_get_session()
198+
)
197199
for ref in references:
198200
if "_cls" in ref:
199201
doc = get_document(ref["_cls"])._from_son(ref)

0 commit comments

Comments
 (0)