Skip to content

Commit 2a60ff9

Browse files
author
James William Pye
committed
Remove __del__ methods.
Use weakref.ref's callback capability to register closed statements and cursors to avoid reference leaks due to circular references. Fixes: http://pgfoundry.org/tracker/?func=detail&atid=442&aid=1010604&group_id=1000094 Thanks goes to: Valentine Gogichashvili
1 parent 60e0503 commit 2a60ff9

2 files changed

Lines changed: 56 additions & 35 deletions

File tree

postgresql/driver/pq3.py

Lines changed: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88
import sys
99
import os
10+
import weakref
1011
import warnings
1112
import collections
1213

@@ -202,13 +203,19 @@ def _init(self):
202203
"""
203204

204205
def __init__(self, cursor_id):
206+
self.cursor_id = cursor_id
205207
if self.statement is not None:
206208
self._output = self.statement._output
207209
self._output_io = self.statement._output_io
208210
self._output_formats = self.statement._output_formats or ()
209211
self._output_attmap = self.statement._output_attmap
210212

211-
self.cursor_id = cursor_id
213+
if self.cursor_id == ID(self):
214+
addgarbage = self.database.pq.garbage_cursors.append
215+
typio = self.database.typio
216+
self._del = weakref.ref(
217+
self, lambda x: addgarbage(typio.encode(cursor_id))
218+
)
212219
self._quoted_cursor_id = '"' + self.cursor_id.replace('"', '""') + '"'
213220
self._pq_cursor_id = self.database.typio.encode(self.cursor_id)
214221
self._init()
@@ -221,19 +228,12 @@ def close(self):
221228
self.database.pq.garbage_cursors.append(
222229
self.database.typio.encode(self.cursor_id)
223230
)
224-
self.closed = True
225-
226-
def __del__(self):
227-
if not self.closed and ID(self) == self.cursor_id:
228-
self.database.pq.garbage_cursors.append(
229-
self.database.typio.encode(self.cursor_id)
230-
)
231-
232-
def _asynchook(self, *args):
233-
self.database._receive_async(*args, controller = self)
231+
self.closed = True
232+
if hasattr(self, '_del'):
233+
del self._del
234234

235235
def _ins(self, *args):
236-
return xact.Instruction(*args, asynchook = self._asynchook)
236+
return xact.Instruction(*args, asynchook = self.database._receive_async)
237237

238238
def _pq_xp_describe(self):
239239
return (element.DescribePortal(self._pq_cursor_id),)
@@ -435,6 +435,9 @@ def _init(self):
435435
elif x.type is element.Complete.type:
436436
self._complete_message = x
437437
self.database._pq_complete()
438+
# If this was a select/copy cursor,
439+
# the data messages would have caused an earlier
440+
# return.
438441
self._xact = None
439442
return
440443
elif x.type is expect:
@@ -468,7 +471,6 @@ def __next__(self):
468471
if not x.completed:
469472
# Transaction has been cleaned out of completed? iterator is done.
470473
self._xact = None
471-
self.closed = True
472474
raise StopIteration
473475

474476
chunk = x.completed[0][1]
@@ -526,7 +528,6 @@ def __next__(self):
526528
self.database._pq_push(self._xact, self)
527529
else:
528530
# it's done.
529-
self.close()
530531
self._xact = None
531532
if not chunk:
532533
raise StopIteration
@@ -706,8 +707,13 @@ def __init__(self, database, statement_id, string):
706707
self._pq_statement_id = None
707708
self.closed = None
708709

709-
def _receive_async(self, *args):
710-
return self.database._receive_async(*args, controller = self)
710+
if not statement_id:
711+
addgarbage = database.pq.garbage_statements.append
712+
typio = database.typio
713+
sid = self.statement_id
714+
self._del = weakref.ref(
715+
self, lambda x: addgarbage(typio.encode(sid))
716+
)
711717

712718
def __repr__(self):
713719
return '<{mod}.{name}[{ci}] {state}>'.format(
@@ -837,18 +843,11 @@ def sql_parameter_types(self):
837843
]
838844

839845
def close(self):
840-
if not (self.closed is True):
846+
if self.closed is False:
841847
self.database.pq.garbage_statements.append(self._pq_statement_id)
842848
self.closed = True
843-
844-
def __del__(self):
845-
# Only close statements that have generated IDs as the ones
846-
# with explicitly created
847-
if not self.closed and ID(self) == self.statement_id:
848-
# Always close CPSs as the way the statement_id is generated
849-
# might cause a conflict if Python were to reuse the previously
850-
# used id()[it can and has happened]. - jwp 2007
851-
self.close()
849+
if hasattr(self, '_del'):
850+
del self._del
852851

853852
def _init(self):
854853
"""
@@ -873,7 +872,7 @@ def _init(self):
873872
element.SynchronizeMessage,
874873
)
875874
)
876-
self._xact = xact.Instruction(cmd, asynchook = self._receive_async)
875+
self._xact = xact.Instruction(cmd, asynchook = self.database._receive_async)
877876
self.database._pq_push(self._xact, self)
878877

879878
def _fini(self):
@@ -886,7 +885,7 @@ def _fini(self):
886885
if self._xact is self.database.pq.xact:
887886
try:
888887
self.database._pq_complete()
889-
except:
888+
except Exception:
890889
self.closed = True
891890
raise
892891

@@ -997,7 +996,7 @@ def first(self, *parameters):
997996
len(self._input), len(parameters)
998997
))
999998
# Parameters? Build em'.
1000-
c = self.database
999+
db = self.database
10011000

10021001
if self._input_io:
10031002
params = pg_typio.process_tuple(
@@ -1019,10 +1018,10 @@ def first(self, *parameters):
10191018
element.Execute(b'', 0xFFFFFFFF),
10201019
element.SynchronizeMessage
10211020
),
1022-
asynchook = self._receive_async
1021+
asynchook = db._receive_async
10231022
)
1024-
c._pq_push(x, self)
1025-
c._pq_complete()
1023+
db._pq_push(x, self)
1024+
db._pq_complete()
10261025

10271026
if self._output_io:
10281027
##
@@ -1078,7 +1077,7 @@ def _copy_data_in(self,
10781077
element.Execute(b'', 1),
10791078
element.SynchronizeMessage,
10801079
),
1081-
asynchook = self._receive_async
1080+
asynchook = self.database._receive_async
10821081
)
10831082
self.database._pq_push(x, self)
10841083

@@ -1153,7 +1152,7 @@ def _load_bulk_tuples(self, tupleseq, tps = None):
11531152
last = element.SynchronizeMessage
11541153
xm.append(last)
11551154
self.database._pq_push(
1156-
xact.Instruction(xm, asynchook = self._receive_async),
1155+
xact.Instruction(xm, asynchook = self.database._receive_async),
11571156
self
11581157
)
11591158
self.database._pq_complete()
@@ -2119,7 +2118,7 @@ def _error_lookup(self, om : element.Error,) -> pg_exc.Error:
21192118
return err
21202119

21212120
def _receive_async(self, msg, controller = None):
2122-
c = controller or self
2121+
c = controller or getattr(self, '_controller', self)
21232122
if msg.type is element.ShowOption.type:
21242123
if msg.name == b'client_encoding':
21252124
self.typio.set_encoding(msg.value.decode('ascii'))

postgresql/test/test_driver.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sys
66
import os
77
import unittest
8+
import gc
89
import threading
910
import time
1011
import datetime
@@ -317,6 +318,27 @@ def testItsClosed(self):
317318
c.close()
318319
self.db.close()
319320

321+
def testGarbage(self):
322+
ps = self.db.prepare('select 1')
323+
sid = ps.statement_id
324+
ci = ps.chunks()
325+
ci_id = ci.cursor_id
326+
c = ps.declare()
327+
cid = c.cursor_id
328+
# make sure there are no remaining xact references..
329+
self.db._pq_complete()
330+
# ci and c both hold references to ps, so they must
331+
# be removed before we can observe the effects __del__
332+
del c
333+
gc.collect()
334+
self.failUnless(self.db.typio.encode(cid) in self.db.pq.garbage_cursors)
335+
del ci
336+
gc.collect()
337+
self.failUnless(self.db.typio.encode(ci_id) in self.db.pq.garbage_cursors)
338+
del ps
339+
gc.collect()
340+
self.failUnless(self.db.typio.encode(sid) in self.db.pq.garbage_statements)
341+
320342
def testStatementCall(self):
321343
ps = self.db.prepare("SELECT 1")
322344
r = ps()

0 commit comments

Comments
 (0)