diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e63edb73..7f62a8bc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ ## Unreleased ### Added +- Added `getMemUsed()`, `getMemTotal()`, and `getMemExternEstim()` methods ### Fixed +- Removed incorrect `Py_INCREF`/`Py_DECREF` on `Model` in `catchEvent`/`dropEvent` that caused reference count imbalance ### Changed ### Removed diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index db862467c..bdea41bd5 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -11268,7 +11268,6 @@ cdef class Model: else: raise Warning("event handler not found") - Py_INCREF(self) PY_SCIP_CALL(SCIPcatchEvent(self._scip, eventtype, _eventhdlr, NULL, NULL)) def dropEvent(self, eventtype, Eventhdlr eventhdlr): @@ -11288,7 +11287,6 @@ cdef class Model: else: raise Warning("event handler not found") - Py_DECREF(self) PY_SCIP_CALL(SCIPdropEvent(self._scip, eventtype, _eventhdlr, NULL, -1)) def catchVarEvent(self, Variable var, eventtype, Eventhdlr eventhdlr): diff --git a/tests/test_event.py b/tests/test_event.py index 7b11980b9..1b54209d0 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -1,4 +1,4 @@ -import pytest, random +import pytest, weakref, gc, random from pyscipopt import Model, Eventhdlr, SCIP_RESULT, SCIP_EVENTTYPE, SCIP_PARAMSETTING, quicksum @@ -189,4 +189,40 @@ def eventexec(self, event): m.includeEventhdlr(ev, "var_event", "event handler for var events") with pytest.raises(Exception): - m.optimize() \ No newline at end of file + m.optimize() + +def test_catchEvent_does_not_leak_model(): + """catchEvent should not artificially increment the Model's reference count. + + Previously, catchEvent called Py_INCREF(self) on the Model, and dropEvent + called Py_DECREF(self). Since many event handlers skip dropEvent (e.g. when + self.model is already dead), this caused the Model to leak — its refcount + never returned to zero, preventing garbage collection. + """ + + class SimpleEvent(Eventhdlr): + def eventinit(self): + self.model.catchEvent(SCIP_EVENTTYPE.NODEFOCUSED, self) + + def eventexit(self): + pass # intentionally no dropEvent, which is bad practice + + def eventexec(self, event): + pass + + m = Model() + m.hideOutput() + ev = SimpleEvent() + m.includeEventhdlr(ev, "simple", "test event handler") + m.addVar("x", obj=1, vtype="I") + m.optimize() + + ref = weakref.ref(m) + + del ev + gc.collect() + assert ref() is not None, "Model was garbage collected — event handler absorbed a reference" + + del m + gc.collect() + assert ref() is None, "Model was not garbage collected — catchEvent likely leaked a reference" \ No newline at end of file