Skip to content

Commit 5d92b2f

Browse files
authored
PYTHON-2243 Raise informative error message when attempting a GridFS operation in a transaction (#454)
1 parent 7e2790c commit 5d92b2f

File tree

4 files changed

+87
-9
lines changed

4 files changed

+87
-9
lines changed

doc/changelog.rst

+7
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ Version 3.11 adds support for MongoDB 4.4. Highlights include:
5353
:meth:`~pymongo.database.Database.command` to run the ``reIndex`` command
5454
instead.
5555

56+
Unavoidable breaking changes:
57+
58+
- :class:`~gridfs.GridFSBucket` and :class:`~gridfs.GridFS` do not support
59+
multi-document transactions. Running a GridFS operation in a transaction
60+
now always raises the following error:
61+
``InvalidOperation: GridFS does not support multi-document transactions``
62+
5663
.. _validate command: https://docs.mongodb.com/manual/reference/command/validate/
5764

5865
Issues Resolved

gridfs/__init__.py

+18-7
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
GridOut,
2727
GridOutCursor,
2828
DEFAULT_CHUNK_SIZE,
29-
_clear_entity_type_registry)
29+
_clear_entity_type_registry,
30+
_disallow_transactions)
3031
from pymongo import (ASCENDING,
3132
DESCENDING)
3233
from pymongo.common import UNAUTHORIZED_CODES, validate_string
@@ -50,6 +51,10 @@ def __init__(self, database, collection="fs", disable_md5=False):
5051
computed for uploaded files. Useful in environments where MD5
5152
cannot be used for regulatory or other reasons. Defaults to False.
5253
54+
.. versionchanged:: 3.11
55+
Running a GridFS operation in a transaction now always raises an
56+
error. GridFS does not support multi-document transactions.
57+
5358
.. versionchanged:: 3.1
5459
Indexes are only ensured on the first write to the DB.
5560
@@ -68,7 +73,6 @@ def __init__(self, database, collection="fs", disable_md5=False):
6873
raise ConfigurationError('database must use '
6974
'acknowledged write_concern')
7075

71-
self.__database = database
7276
self.__collection = database[collection]
7377
self.__files = self.__collection.files
7478
self.__chunks = self.__collection.chunks
@@ -88,8 +92,6 @@ def new_file(self, **kwargs):
8892
:Parameters:
8993
- `**kwargs` (optional): keyword arguments for file creation
9094
"""
91-
# No need for __ensure_index_files_id() here; GridIn ensures
92-
# the (files_id, n) index when needed.
9395
return GridIn(
9496
self.__collection, disable_md5=self.__disable_md5, **kwargs)
9597

@@ -192,6 +194,7 @@ def get_version(self, filename=None, version=-1, session=None, **kwargs):
192194
if filename is not None:
193195
query["filename"] = filename
194196

197+
_disallow_transactions(session)
195198
cursor = self.__files.find(query, session=session)
196199
if version < 0:
197200
skip = abs(version) - 1
@@ -249,6 +252,7 @@ def delete(self, file_id, session=None):
249252
.. versionchanged:: 3.1
250253
``delete`` no longer ensures indexes.
251254
"""
255+
_disallow_transactions(session)
252256
self.__files.delete_one({"_id": file_id}, session=session)
253257
self.__chunks.delete_many({"files_id": file_id}, session=session)
254258

@@ -266,6 +270,7 @@ def list(self, session=None):
266270
.. versionchanged:: 3.1
267271
``list`` no longer ensures indexes.
268272
"""
273+
_disallow_transactions(session)
269274
# With an index, distinct includes documents with no filename
270275
# as None.
271276
return [
@@ -299,6 +304,7 @@ def find_one(self, filter=None, session=None, *args, **kwargs):
299304
if filter is not None and not isinstance(filter, abc.Mapping):
300305
filter = {"_id": filter}
301306

307+
_disallow_transactions(session)
302308
for f in self.find(filter, *args, session=session, **kwargs):
303309
return f
304310

@@ -403,6 +409,7 @@ def exists(self, document_or_id=None, session=None, **kwargs):
403409
.. versionchanged:: 3.6
404410
Added ``session`` parameter.
405411
"""
412+
_disallow_transactions(session)
406413
if kwargs:
407414
f = self.__files.find_one(kwargs, ["_id"], session=session)
408415
else:
@@ -439,6 +446,10 @@ def __init__(self, db, bucket_name="fs",
439446
computed for uploaded files. Useful in environments where MD5
440447
cannot be used for regulatory or other reasons. Defaults to False.
441448
449+
.. versionchanged:: 3.11
450+
Running a GridFS operation in a transaction now always raises an
451+
error. GridFSBucket does not support multi-document transactions.
452+
442453
.. versionadded:: 3.1
443454
444455
.. mongodoc:: gridfs
@@ -452,7 +463,6 @@ def __init__(self, db, bucket_name="fs",
452463
if not wtc.acknowledged:
453464
raise ConfigurationError('write concern must be acknowledged')
454465

455-
self._db = db
456466
self._bucket_name = bucket_name
457467
self._collection = db[bucket_name]
458468
self._disable_md5 = disable_md5
@@ -746,6 +756,7 @@ def delete(self, file_id, session=None):
746756
.. versionchanged:: 3.6
747757
Added ``session`` parameter.
748758
"""
759+
_disallow_transactions(session)
749760
res = self._files.delete_one({"_id": file_id}, session=session)
750761
self._chunks.delete_many({"files_id": file_id}, session=session)
751762
if not res.deleted_count:
@@ -839,9 +850,8 @@ def open_download_stream_by_name(self, filename, revision=-1, session=None):
839850
Added ``session`` parameter.
840851
"""
841852
validate_string("filename", filename)
842-
843853
query = {"filename": filename}
844-
854+
_disallow_transactions(session)
845855
cursor = self._files.find(query, session=session)
846856
if revision < 0:
847857
skip = abs(revision) - 1
@@ -922,6 +932,7 @@ def rename(self, file_id, new_filename, session=None):
922932
.. versionchanged:: 3.6
923933
Added ``session`` parameter.
924934
"""
935+
_disallow_transactions(session)
925936
result = self._files.update_one({"_id": file_id},
926937
{"$set": {"filename": new_filename}},
927938
session=session)

gridfs/grid_file.py

+14
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from pymongo.errors import (ConfigurationError,
3232
CursorNotFound,
3333
DuplicateKeyError,
34+
InvalidOperation,
3435
OperationFailure)
3536
from pymongo.read_preferences import ReadPreference
3637

@@ -105,6 +106,12 @@ def _clear_entity_type_registry(entity, **kwargs):
105106
return entity.with_options(codec_options=codecopts, **kwargs)
106107

107108

109+
def _disallow_transactions(session):
110+
if session and session.in_transaction:
111+
raise InvalidOperation(
112+
'GridFS does not support multi-document transactions')
113+
114+
108115
class GridIn(object):
109116
"""Class to write data to GridFS.
110117
"""
@@ -168,6 +175,7 @@ def __init__(
168175
if not root_collection.write_concern.acknowledged:
169176
raise ConfigurationError('root_collection must use '
170177
'acknowledged write_concern')
178+
_disallow_transactions(session)
171179

172180
# Handle alternative naming
173181
if "content_type" in kwargs:
@@ -207,6 +215,7 @@ def __create_index(self, collection, index_key, unique):
207215

208216
def __ensure_indexes(self):
209217
if not object.__getattribute__(self, "_ensured_index"):
218+
_disallow_transactions(self._session)
210219
self.__create_index(self._coll.files, _F_INDEX, False)
211220
self.__create_index(self._coll.chunks, _C_INDEX, True)
212221
object.__setattr__(self, "_ensured_index", True)
@@ -456,6 +465,7 @@ def __init__(self, root_collection, file_id=None, file_document=None,
456465
if not isinstance(root_collection, Collection):
457466
raise TypeError("root_collection must be an "
458467
"instance of Collection")
468+
_disallow_transactions(session)
459469

460470
root_collection = _clear_entity_type_registry(root_collection)
461471

@@ -483,6 +493,7 @@ def __init__(self, root_collection, file_id=None, file_document=None,
483493

484494
def _ensure_file(self):
485495
if not self._file:
496+
_disallow_transactions(self._session)
486497
self._file = self.__files.find_one({"_id": self.__file_id},
487498
session=self._session)
488499
if not self._file:
@@ -718,6 +729,7 @@ def _create_cursor(self):
718729
filter = {"files_id": self._id}
719730
if self._next_chunk > 0:
720731
filter["n"] = {"$gte": self._next_chunk}
732+
_disallow_transactions(self._session)
721733
self._cursor = self._chunks.find(filter, sort=[("n", 1)],
722734
session=self._session)
723735

@@ -810,6 +822,7 @@ def __init__(self, collection, filter=None, skip=0, limit=0,
810822
811823
.. mongodoc:: cursors
812824
"""
825+
_disallow_transactions(session)
813826
collection = _clear_entity_type_registry(collection)
814827

815828
# Hold on to the base "fs" collection to create GridOut objects later.
@@ -823,6 +836,7 @@ def __init__(self, collection, filter=None, skip=0, limit=0,
823836
def next(self):
824837
"""Get next GridOut object from cursor.
825838
"""
839+
_disallow_transactions(self.session)
826840
# Work around "super is not iterable" issue in Python 3.x
827841
next_file = super(GridOutCursor, self).next()
828842
return GridOut(self.__root_collection, file_document=next_file,

test/test_transactions.py

+48-2
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,21 @@
1919

2020
sys.path[0:0] = [""]
2121

22+
from bson.py3compat import StringIO
23+
2224
from pymongo import client_session, WriteConcern
2325
from pymongo.client_session import TransactionOptions
2426
from pymongo.errors import (CollectionInvalid,
2527
ConfigurationError,
2628
ConnectionFailure,
29+
InvalidOperation,
2730
OperationFailure)
2831
from pymongo.operations import IndexModel, InsertOne
2932
from pymongo.read_concern import ReadConcern
3033
from pymongo.read_preferences import ReadPreference
3134

35+
from gridfs import GridFS, GridFSBucket
36+
3237
from test import unittest, client_context
3338
from test.utils import (rs_client, single_client,
3439
wait_until, OvertCommandListener,
@@ -215,8 +220,7 @@ def test_unpin_for_non_transaction_operation(self):
215220
@client_context.require_transactions
216221
@client_context.require_version_min(4, 3, 4)
217222
def test_create_collection(self):
218-
client = rs_client()
219-
self.addCleanup(client.close)
223+
client = client_context.client
220224
db = client.pymongo_test
221225
coll = db.test_create_collection
222226
self.addCleanup(coll.drop)
@@ -241,6 +245,48 @@ def create_and_insert(session):
241245
db.create_collection(coll.name, session=s)
242246
self.assertEqual(ctx.exception.code, 48) # NamespaceExists
243247

248+
@client_context.require_transactions
249+
def test_gridfs_does_not_support_transactions(self):
250+
client = client_context.client
251+
db = client.pymongo_test
252+
gfs = GridFS(db)
253+
bucket = GridFSBucket(db)
254+
255+
def gridfs_find(*args, **kwargs):
256+
return gfs.find(*args, **kwargs).next()
257+
258+
def gridfs_open_upload_stream(*args, **kwargs):
259+
bucket.open_upload_stream(*args, **kwargs).write(b'1')
260+
261+
gridfs_ops = [
262+
(gfs.put, (b'123',)),
263+
(gfs.get, (1,)),
264+
(gfs.get_version, ('name',)),
265+
(gfs.get_last_version, ('name',)),
266+
(gfs.delete, (1, )),
267+
(gfs.list, ()),
268+
(gfs.find_one, ()),
269+
(gridfs_find, ()),
270+
(gfs.exists, ()),
271+
(gridfs_open_upload_stream, ('name',)),
272+
(bucket.upload_from_stream, ('name', b'data',)),
273+
(bucket.download_to_stream, (1, StringIO(),)),
274+
(bucket.download_to_stream_by_name, ('name', StringIO(),)),
275+
(bucket.delete, (1,)),
276+
(bucket.find, ()),
277+
(bucket.open_download_stream, (1,)),
278+
(bucket.open_download_stream_by_name, ('name',)),
279+
(bucket.rename, (1, 'new-name',)),
280+
]
281+
282+
with client.start_session() as s, s.start_transaction():
283+
for op, args in gridfs_ops:
284+
with self.assertRaisesRegex(
285+
InvalidOperation,
286+
'GridFS does not support multi-document transactions',
287+
):
288+
op(*args, session=s)
289+
244290

245291
class PatchSessionTimeout(object):
246292
"""Patches the client_session's with_transaction timeout for testing."""

0 commit comments

Comments
 (0)