]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Use Py_TPFLAGS_HAVE_GC for Row
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 24 Jun 2021 13:12:31 +0000 (09:12 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 24 Jun 2021 13:12:31 +0000 (09:12 -0400)
Fixed an issue in the C extension for the :class:`_result.Row` class which
could lead to a memory leak in the unlikely case of a :class:`_result.Row`
object which referred to an ORM object that then was mutated to refer back
to the ``Row`` itself, creating a cycle. The Python C APIs for tracking GC
cycles has been added to the native :class:`_result.Row` implementation to
accommodate for this case.

Fixes: #5348
Change-Id: I3ac32012f29fbb59f8921cf2a124fa3a7ac5f0d1

doc/build/changelog/unreleased_14/5348.rst [new file with mode: 0644]
lib/sqlalchemy/cextension/resultproxy.c
test/aaa_profiling/test_memusage.py

diff --git a/doc/build/changelog/unreleased_14/5348.rst b/doc/build/changelog/unreleased_14/5348.rst
new file mode 100644 (file)
index 0000000..337620a
--- /dev/null
@@ -0,0 +1,11 @@
+.. change::
+    :tags: bug, engine
+    :tickets: 5348
+
+    Fixed an issue in the C extension for the :class:`_result.Row` class which
+    could lead to a memory leak in the unlikely case of a :class:`_result.Row`
+    object which referred to an ORM object that then was mutated to refer back
+    to the ``Row`` itself, creating a cycle. The Python C APIs for tracking GC
+    cycles has been added to the native :class:`_result.Row` implementation to
+    accommodate for this case.
+
index 596b264d03d3a623865fe1eab99c22a822cf0266..d869a058b79122468857b6275006a33aa632ad6b 100644 (file)
@@ -178,6 +178,22 @@ BaseRow_init(BaseRow *self, PyObject *args, PyObject *kwds)
     Py_INCREF(keymap);
     self->keymap = keymap;
     self->key_style = PyLong_AsLong(key_style);
+
+    // observation: because we have not implemented our own new method,
+    // cPython is apparently already calling PyObject_GC_Track for us.
+    // We assume it also called PyObject_GC_New since prior to #5348 we
+    // were already relying upon it to call PyObject_New, and we have now
+    // set Py_TPFLAGS_HAVE_GC.
+
+    return 0;
+}
+
+static int
+BaseRow_traverse(BaseRow *self, visitproc visit, void *arg)
+{
+    Py_VISIT(self->parent);
+    Py_VISIT(self->row);
+    Py_VISIT(self->keymap);
     return 0;
 }
 
@@ -258,14 +274,12 @@ BaseRow_filter_on_values(BaseRow *self, PyObject *filters)
 static void
 BaseRow_dealloc(BaseRow *self)
 {
+    PyObject_GC_UnTrack(self);
     Py_XDECREF(self->parent);
     Py_XDECREF(self->row);
     Py_XDECREF(self->keymap);
-#if PY_MAJOR_VERSION >= 3
-    Py_TYPE(self)->tp_free((PyObject *)self);
-#else
-    self->ob_type->tp_free((PyObject *)self);
-#endif
+    PyObject_GC_Del(self);
+
 }
 
 static PyObject *
@@ -760,9 +774,9 @@ static PyTypeObject BaseRowType = {
     (getattrofunc)BaseRow_getattro,/* tp_getattro */
     0,                                  /* tp_setattro */
     0,                                  /* tp_as_buffer */
-    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,               /* tp_flags */
+    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, /* tp_flags */
     "BaseRow is a abstract base class for Row",   /* tp_doc */
-    0,                                  /* tp_traverse */
+    (traverseproc)BaseRow_traverse,           /* tp_traverse */
     0,                                  /* tp_clear */
     0,                                  /* tp_richcompare */
     0,                                  /* tp_weaklistoffset */
index 28865d0109a2d8358a738af2a99098a79f84350c..2fedcdfdc522f85700000ab34145aa36dd21c2de 100644 (file)
@@ -14,6 +14,7 @@ from sqlalchemy import String
 from sqlalchemy import testing
 from sqlalchemy import Unicode
 from sqlalchemy import util
+from sqlalchemy.engine import result
 from sqlalchemy.orm import aliased
 from sqlalchemy.orm import clear_mappers
 from sqlalchemy.orm import configure_mappers
@@ -293,6 +294,24 @@ class MemUsageTest(EnsureZeroed):
 
         go()
 
+    @testing.requires.cextensions
+    def test_cycles_in_row(self):
+
+        tup = result.result_tuple(["a", "b", "c"])
+
+        @profile_memory()
+        def go():
+            obj = {"foo": {}}
+            obj["foo"]["bar"] = obj
+
+            row = tup([1, 2, obj])
+
+            obj["foo"]["row"] = row
+
+            del row
+
+        go()
+
     def test_ad_hoc_types(self):
         """test storage of bind processors, result processors
         in dialect-wide registry."""