Skip to content

Commit cb9def3

Browse files
committed
Replace weakref.proxy with strong references for plugin self.model (#1193)
1 parent b31fc2c commit cb9def3

File tree

6 files changed

+139
-71
lines changed

6 files changed

+139
-71
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Added `getMemUsed()`, `getMemTotal()`, and `getMemExternEstim()` methods
66
### Fixed
77
- Removed `Py_INCREF`/`Py_DECREF` on `Model` in `catchEvent`/`dropEvent` that caused memory leak for imbalanced usage
8+
- Replaced `weakref.proxy` with strong references for plugin `self.model`, fixing `ReferenceError` during cleanup callbacks (#1193)
89
### Changed
910
### Removed
1011

src/pyscipopt/scip.pxd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2249,11 +2249,14 @@ cdef class Model:
22492249
cdef int _generated_event_handlers_count
22502250
# store references to Benders subproblem Models for proper cleanup
22512251
cdef _benders_subproblems
2252+
# store references to plugins to break circular references in __dealloc__
2253+
cdef _plugins
22522254
# store iis, if found
22532255
cdef SCIP_IIS* _iis
22542256
# helper methods for later var and cons cleanup
22552257
cdef _getOrCreateCons(self, SCIP_CONS* scip_cons)
22562258
cdef _getOrCreateVar(self, SCIP_VAR* scip_var)
2259+
cdef _free_scip_instance(self)
22572260

22582261
@staticmethod
22592262
cdef create(SCIP* scip)

src/pyscipopt/scip.pxi

Lines changed: 67 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
##@file scip.pxi
22
#@brief holding functions in python that reference the SCIP public functions included in scip.pxd
3-
import weakref
43
from os.path import abspath
54
from os.path import splitext
65
import os
@@ -2821,6 +2820,7 @@ cdef class Model:
28212820
self._modelconss = {}
28222821
self._generated_event_handlers_count = 0
28232822
self._benders_subproblems = [] # Keep references to Benders subproblem Models
2823+
self._plugins = [] # Keep references to plugins to break cycles in __dealloc__
28242824
self._iis = NULL
28252825

28262826
if not createscip:
@@ -2892,15 +2892,30 @@ cdef class Model:
28922892

28932893
self.includeEventhdlr(event_handler, name, description)
28942894

2895+
def __del__(self):
2896+
"""Free SCIP before the cyclic GC clears Python references.
2897+
2898+
__del__ (tp_finalize) runs before tp_clear, so _plugins and other
2899+
Python attributes are still intact. This lets SCIPfree call plugin
2900+
callbacks (consfree, eventexit, etc.) safely. The GC handles breaking
2901+
the circular references afterwards via tp_clear.
2902+
"""
2903+
self._free_scip_instance()
2904+
28952905
def __dealloc__(self):
2896-
# Declare all C variables at the beginning for Cython compatibility
2906+
# Safety net: if __del__ didn't run (e.g. interpreter shutdown),
2907+
# free SCIP here. Plugin callbacks may not work at this point since
2908+
# tp_clear may have already cleared _plugins.
2909+
if self._scip is not NULL and self._freescip and PY_SCIP_CALL:
2910+
PY_SCIP_CALL( SCIPfree(&self._scip) )
2911+
2912+
cdef _free_scip_instance(self):
2913+
"""Free the SCIP instance. Does not touch Python object references."""
28972914
cdef SCIP_BENDERS** benders
28982915
cdef int nbenders
28992916
cdef int nsubproblems
29002917
cdef int i, j
29012918

2902-
# call C function directly, because we can no longer call this object's methods, according to
2903-
# http://docs.cython.org/src/reference/extension_types.html#finalization-dealloc
29042919
if self._scip is not NULL and self._freescip and PY_SCIP_CALL:
29052920
# Free Benders subproblems before freeing the main SCIP instance
29062921
if self._benders_subproblems:
@@ -2912,20 +2927,17 @@ cdef class Model:
29122927
for j in range(nsubproblems):
29132928
PY_SCIP_CALL(SCIPfreeBendersSubproblem(self._scip, benders[i], j))
29142929

2915-
# Clear the references to allow Python GC to clean up the Model objects
2916-
self._benders_subproblems = []
2930+
SCIPfree(&self._scip)
2931+
self._scip = NULL
2932+
self._freescip = False
29172933

2918-
# Invalidate all variable and constraint pointers before freeing SCIP. See issue #604.
2934+
# Invalidate all variable and constraint pointers after freeing SCIP. See issue #604.
29192935
if self._modelvars:
29202936
for var in self._modelvars.values():
29212937
(<Variable>var).scip_var = NULL
2922-
self._modelvars = {}
29232938
if self._modelconss:
29242939
for cons in self._modelconss.values():
29252940
(<Constraint>cons).scip_cons = NULL
2926-
self._modelconss = {}
2927-
2928-
PY_SCIP_CALL( SCIPfree(&self._scip) )
29292941

29302942
def __hash__(self):
29312943
return hash(<size_t>self._scip)
@@ -3070,6 +3082,21 @@ cdef class Model:
30703082
"""Frees problem and solution process data."""
30713083
PY_SCIP_CALL(SCIPfreeProb(self._scip))
30723084

3085+
def free(self):
3086+
"""Explicitly free SCIP instance and break circular references with plugins.
3087+
3088+
This method should be called when you want to ensure deterministic cleanup.
3089+
All SCIP callbacks (consfree, eventexit, etc.) are executed during this call.
3090+
After calling this method, the Model object should not be used anymore.
3091+
"""
3092+
self._free_scip_instance()
3093+
3094+
# Break circular references with plugins
3095+
if self._plugins:
3096+
for plugin in self._plugins:
3097+
plugin.model = None
3098+
self._plugins = []
3099+
30733100
def freeTransform(self):
30743101
"""Frees all solution process data including presolving and
30753102
transformed problem, only original problem is kept."""
@@ -9191,9 +9218,9 @@ cdef class Model:
91919218
PyEventDelete,
91929219
PyEventExec,
91939220
<SCIP_EVENTHDLRDATA*>eventhdlr))
9194-
eventhdlr.model = <Model>weakref.proxy(self)
9221+
eventhdlr.model = self
91959222
eventhdlr.name = name
9196-
Py_INCREF(eventhdlr)
9223+
self._plugins.append(eventhdlr)
91979224

91989225
def includePricer(self, Pricer pricer, name, desc, priority=1, delay=True):
91999226
"""
@@ -9223,8 +9250,8 @@ cdef class Model:
92239250
cdef SCIP_PRICER* scip_pricer
92249251
scip_pricer = SCIPfindPricer(self._scip, n)
92259252
PY_SCIP_CALL(SCIPactivatePricer(self._scip, scip_pricer))
9226-
pricer.model = <Model>weakref.proxy(self)
9227-
Py_INCREF(pricer)
9253+
pricer.model = self
9254+
self._plugins.append(pricer)
92289255
pricer.scip_pricer = scip_pricer
92299256

92309257
def includeConshdlr(self, Conshdlr conshdlr, name, desc, sepapriority=0,
@@ -9283,9 +9310,9 @@ cdef class Model:
92839310
PyConsActive, PyConsDeactive, PyConsEnable, PyConsDisable, PyConsDelvars, PyConsPrint, PyConsCopy,
92849311
PyConsParse, PyConsGetvars, PyConsGetnvars, PyConsGetdivebdchgs, PyConsGetPermSymGraph, PyConsGetSignedPermSymGraph,
92859312
<SCIP_CONSHDLRDATA*>conshdlr))
9286-
conshdlr.model = <Model>weakref.proxy(self)
9313+
conshdlr.model = self
92879314
conshdlr.name = name
9288-
Py_INCREF(conshdlr)
9315+
self._plugins.append(conshdlr)
92899316

92909317
def deactivatePricer(self, Pricer pricer):
92919318
"""
@@ -9450,8 +9477,8 @@ cdef class Model:
94509477
d = str_conversion(desc)
94519478
PY_SCIP_CALL(SCIPincludePresol(self._scip, n, d, priority, maxrounds, timing, PyPresolCopy, PyPresolFree, PyPresolInit,
94529479
PyPresolExit, PyPresolInitpre, PyPresolExitpre, PyPresolExec, <SCIP_PRESOLDATA*>presol))
9453-
presol.model = <Model>weakref.proxy(self)
9454-
Py_INCREF(presol)
9480+
presol.model = self
9481+
self._plugins.append(presol)
94559482

94569483
def includeSepa(self, Sepa sepa, name, desc, priority=0, freq=10, maxbounddist=1.0, usessubscip=False, delay=False):
94579484
"""
@@ -9493,9 +9520,9 @@ cdef class Model:
94939520
d = str_conversion(desc)
94949521
PY_SCIP_CALL(SCIPincludeSepa(self._scip, n, d, priority, freq, maxbounddist, usessubscip, delay, PySepaCopy, PySepaFree,
94959522
PySepaInit, PySepaExit, PySepaInitsol, PySepaExitsol, PySepaExeclp, PySepaExecsol, <SCIP_SEPADATA*>sepa))
9496-
sepa.model = <Model>weakref.proxy(self)
9523+
sepa.model = self
94979524
sepa.name = name
9498-
Py_INCREF(sepa)
9525+
self._plugins.append(sepa)
94999526

95009527
def includeReader(self, Reader reader, name, desc, ext):
95019528
"""
@@ -9518,9 +9545,9 @@ cdef class Model:
95189545
e = str_conversion(ext)
95199546
PY_SCIP_CALL(SCIPincludeReader(self._scip, n, d, e, PyReaderCopy, PyReaderFree,
95209547
PyReaderRead, PyReaderWrite, <SCIP_READERDATA*>reader))
9521-
reader.model = <Model>weakref.proxy(self)
9548+
reader.model = self
95229549
reader.name = name
9523-
Py_INCREF(reader)
9550+
self._plugins.append(reader)
95249551

95259552
def includeProp(self, Prop prop, name, desc, presolpriority, presolmaxrounds,
95269553
proptiming, presoltiming=SCIP_PRESOLTIMING_FAST, priority=1, freq=1, delay=True):
@@ -9560,8 +9587,8 @@ cdef class Model:
95609587
PyPropInitpre, PyPropExitpre, PyPropInitsol, PyPropExitsol,
95619588
PyPropPresol, PyPropExec, PyPropResProp,
95629589
<SCIP_PROPDATA*> prop))
9563-
prop.model = <Model>weakref.proxy(self)
9564-
Py_INCREF(prop)
9590+
prop.model = self
9591+
self._plugins.append(prop)
95659592

95669593
def includeHeur(self, Heur heur, name, desc, dispchar, priority=10000, freq=1, freqofs=0,
95679594
maxdepth=-1, timingmask=SCIP_HEURTIMING_BEFORENODE, usessubscip=False):
@@ -9605,9 +9632,9 @@ cdef class Model:
96059632
PyHeurCopy, PyHeurFree, PyHeurInit, PyHeurExit,
96069633
PyHeurInitsol, PyHeurExitsol, PyHeurExec,
96079634
<SCIP_HEURDATA*> heur))
9608-
heur.model = <Model>weakref.proxy(self)
9635+
heur.model = self
96099636
heur.name = name
9610-
Py_INCREF(heur)
9637+
self._plugins.append(heur)
96119638

96129639
def includeIISfinder(self, IISfinder iisfinder, name, desc, priority=10000, freq=1):
96139640
"""
@@ -9639,7 +9666,7 @@ cdef class Model:
96399666

96409667
scip_iisfinder = SCIPfindIISfinder(self._scip, nam)
96419668
iisfinder.name = name
9642-
Py_INCREF(iisfinder)
9669+
self._plugins.append(iisfinder)
96439670
iisfinder.scip_iisfinder = scip_iisfinder
96449671

96459672
def generateIIS(self):
@@ -9697,10 +9724,9 @@ cdef class Model:
96979724
des = str_conversion(desc)
96989725
PY_SCIP_CALL(SCIPincludeRelax(self._scip, nam, des, priority, freq, PyRelaxCopy, PyRelaxFree, PyRelaxInit, PyRelaxExit,
96999726
PyRelaxInitsol, PyRelaxExitsol, PyRelaxExec, <SCIP_RELAXDATA*> relax))
9700-
relax.model = <Model>weakref.proxy(self)
9727+
relax.model = self
97019728
relax.name = name
9702-
9703-
Py_INCREF(relax)
9729+
self._plugins.append(relax)
97049730

97059731
def includeCutsel(self, Cutsel cutsel, name, desc, priority):
97069732
"""
@@ -9725,8 +9751,8 @@ cdef class Model:
97259751
priority, PyCutselCopy, PyCutselFree, PyCutselInit, PyCutselExit,
97269752
PyCutselInitsol, PyCutselExitsol, PyCutselSelect,
97279753
<SCIP_CUTSELDATA*> cutsel))
9728-
cutsel.model = <Model>weakref.proxy(self)
9729-
Py_INCREF(cutsel)
9754+
cutsel.model = self
9755+
self._plugins.append(cutsel)
97309756

97319757
def includeBranchrule(self, Branchrule branchrule, name, desc, priority, maxdepth, maxbounddist):
97329758
"""
@@ -9757,8 +9783,8 @@ cdef class Model:
97579783
PyBranchruleCopy, PyBranchruleFree, PyBranchruleInit, PyBranchruleExit,
97589784
PyBranchruleInitsol, PyBranchruleExitsol, PyBranchruleExeclp, PyBranchruleExecext,
97599785
PyBranchruleExecps, <SCIP_BRANCHRULEDATA*> branchrule))
9760-
branchrule.model = <Model>weakref.proxy(self)
9761-
Py_INCREF(branchrule)
9786+
branchrule.model = self
9787+
self._plugins.append(branchrule)
97629788

97639789
def includeNodesel(self, Nodesel nodesel, name, desc, stdpriority, memsavepriority):
97649790
"""
@@ -9785,8 +9811,8 @@ cdef class Model:
97859811
PyNodeselCopy, PyNodeselFree, PyNodeselInit, PyNodeselExit,
97869812
PyNodeselInitsol, PyNodeselExitsol, PyNodeselSelect, PyNodeselComp,
97879813
<SCIP_NODESELDATA*> nodesel))
9788-
nodesel.model = <Model>weakref.proxy(self)
9789-
Py_INCREF(nodesel)
9814+
nodesel.model = self
9815+
self._plugins.append(nodesel)
97909816

97919817
def includeBenders(self, Benders benders, name, desc, priority=1, cutlp=True, cutpseudo=True, cutrelax=True,
97929818
shareaux=False):
@@ -9825,10 +9851,10 @@ cdef class Model:
98259851
<SCIP_BENDERSDATA*>benders))
98269852
cdef SCIP_BENDERS* scip_benders
98279853
scip_benders = SCIPfindBenders(self._scip, n)
9828-
benders.model = <Model>weakref.proxy(self)
9854+
benders.model = self
98299855
benders.name = name
98309856
benders._benders = scip_benders
9831-
Py_INCREF(benders)
9857+
self._plugins.append(benders)
98329858

98339859
def includeBenderscut(self, Benders benders, Benderscut benderscut, name, desc, priority=1, islpcut=True):
98349860
"""
@@ -9864,11 +9890,10 @@ cdef class Model:
98649890

98659891
cdef SCIP_BENDERSCUT* scip_benderscut
98669892
scip_benderscut = SCIPfindBenderscut(_benders, n)
9867-
benderscut.model = <Model>weakref.proxy(self)
9893+
benderscut.model = self
98689894
benderscut.benders = benders
98699895
benderscut.name = name
9870-
# TODO: It might be necessary in increment the reference to benders i.e Py_INCREF(benders)
9871-
Py_INCREF(benderscut)
9896+
self._plugins.append(benderscut)
98729897

98739898
def getLPBranchCands(self):
98749899
"""

tests/test_conshdlr.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,7 @@ def conslock(self, constraint, locktype, nlockspos, nlocksneg):
6868
calls.add("conslock")
6969
assert id(constraint) in ids
7070

71-
try:
72-
var = self.model.data["x"]
73-
except ReferenceError:
74-
return
75-
71+
var = self.model.data["x"]
7672
n_locks_down = var.getNLocksDown()
7773
n_locks_up = var.getNLocksUp()
7874
n_locks_down_model = var.getNLocksDownType(SCIP_LOCKTYPE.MODEL)
@@ -249,8 +245,9 @@ def create_model():
249245
# solve problem
250246
model.optimize()
251247

252-
# so that consfree gets called
248+
# so that consfree gets called (GC collects the Model ↔ plugin cycle)
253249
del model
250+
import gc; gc.collect()
254251

255252
# check callbacks got called
256253
assert "consenfolp" in calls

0 commit comments

Comments
 (0)