Repository: MongoEngine/mongoengine

Issue:
Default CappedCollection max_size should be rounded to a multiplication of 256
According to mongodb documentation: "MongoDB will raise the provided size to make it an integer multiple of 256". However, the default size used in mongoengine/document.py is 10000000 (which is obviously not a multiple of 256). This causes errors when calling _get_collection() on an already-created capped collection with the default max size:

Traceback (most recent call last):
  ....
  File "/usr/local/lib/python2.7/dist-packages/mongoengine/document.py", line 300, in save
    collection = self._get_collection()
  File "/usr/local/lib/python2.7/dist-packages/mongoengine/document.py", line 186, in _get_collection
    raise InvalidCollectionError(msg)
InvalidCollectionError: Cannot create collection "Collection(Database(MongoClient('localhost', 27017), u'my_db'), u'my_coll')" as a capped collection as it already exists

The problem here is that the default size is 10000000, but mongodb rounded it 10002432, and so the comparison between the two fails. 

Suggested solution: set the default max_size to "real" 10MB, i.e. 10 \* 2**20.



Discussion:

Resolving patch:
diff --git a/mongoengine/document.py b/mongoengine/document.py
--- a/mongoengine/document.py
+++ b/mongoengine/document.py
@@ -114,9 +114,11 @@ class Document(BaseDocument):
     specifying :attr:`max_documents` and :attr:`max_size` in the :attr:`meta`
     dictionary. :attr:`max_documents` is the maximum number of documents that
     is allowed to be stored in the collection, and :attr:`max_size` is the
-    maximum size of the collection in bytes. If :attr:`max_size` is not
+    maximum size of the collection in bytes. :attr:`max_size` is rounded up
+    to the next multiple of 256 by MongoDB internally and mongoengine before.
+    Use also a multiple of 256 to avoid confusions.  If :attr:`max_size` is not
     specified and :attr:`max_documents` is, :attr:`max_size` defaults to
-    10000000 bytes (10MB).
+    10485760 bytes (10MB).
 
     Indexes may be created by specifying :attr:`indexes` in the :attr:`meta`
     dictionary. The value should be a list of field names or tuples of field
@@ -137,7 +139,7 @@ class Document(BaseDocument):
     By default, any extra attribute existing in stored data but not declared
     in your model will raise a :class:`~mongoengine.FieldDoesNotExist` error.
     This can be disabled by setting :attr:`strict` to ``False``
-    in the :attr:`meta` dictionnary.
+    in the :attr:`meta` dictionary.
     """
 
     # The __metaclass__ attribute is removed by 2to3 when running with Python3
@@ -176,8 +178,11 @@ def _get_collection(cls):
             # Create collection as a capped collection if specified
             if cls._meta.get('max_size') or cls._meta.get('max_documents'):
                 # Get max document limit and max byte size from meta
-                max_size = cls._meta.get('max_size') or 10000000  # 10MB default
+                max_size = cls._meta.get('max_size') or 10 * 2 ** 20  # 10MB default
                 max_documents = cls._meta.get('max_documents')
+                # Round up to next 256 bytes as MongoDB would do it to avoid exception
+                if max_size % 256:
+                    max_size = (max_size // 256 + 1) * 256
 
                 if collection_name in db.collection_names():
                     cls._collection = db[collection_name]


Test patch:
diff --git a/tests/document/instance.py b/tests/document/instance.py
--- a/tests/document/instance.py
+++ b/tests/document/instance.py
@@ -88,7 +88,7 @@ class Log(Document):
         options = Log.objects._collection.options()
         self.assertEqual(options['capped'], True)
         self.assertEqual(options['max'], 10)
-        self.assertTrue(options['size'] >= 4096)
+        self.assertEqual(options['size'], 4096)
 
         # Check that the document cannot be redefined with different options
         def recreate_log_document():
@@ -103,6 +103,69 @@ class Log(Document):
 
         Log.drop_collection()
 
+    def test_capped_collection_default(self):
+        """Ensure that capped collections defaults work properly.
+        """
+        class Log(Document):
+            date = DateTimeField(default=datetime.now)
+            meta = {
+                'max_documents': 10,
+            }
+
+        Log.drop_collection()
+
+        # Create a doc to create the collection
+        Log().save()
+
+        options = Log.objects._collection.options()
+        self.assertEqual(options['capped'], True)
+        self.assertEqual(options['max'], 10)
+        self.assertEqual(options['size'], 10 * 2**20)
+
+        # Check that the document with default value can be recreated
+        def recreate_log_document():
+            class Log(Document):
+                date = DateTimeField(default=datetime.now)
+                meta = {
+                    'max_documents': 10,
+                }
+            # Create the collection by accessing Document.objects
+            Log.objects
+        recreate_log_document()
+        Log.drop_collection()
+
+    def test_capped_collection_no_max_size_problems(self):
+        """Ensure that capped collections with odd max_size work properly.
+        MongoDB rounds up max_size to next multiple of 256, recreating a doc
+        with the same spec failed in mongoengine <0.10
+        """
+        class Log(Document):
+            date = DateTimeField(default=datetime.now)
+            meta = {
+                'max_size': 10000,
+            }
+
+        Log.drop_collection()
+
+        # Create a doc to create the collection
+        Log().save()
+
+        options = Log.objects._collection.options()
+        self.assertEqual(options['capped'], True)
+        self.assertTrue(options['size'] >= 10000)
+
+        # Check that the document with odd max_size value can be recreated
+        def recreate_log_document():
+            class Log(Document):
+                date = DateTimeField(default=datetime.now)
+                meta = {
+                    'max_size': 10000,
+                }
+            # Create the collection by accessing Document.objects
+            Log.objects
+        recreate_log_document()
+        Log.drop_collection()
+
     def test_repr(self):
         """Ensure that unicode representation works
         """