Hello everyone.
There's a bug in the NotificationHandler class that leads to delayed
delivery and ultimately missed notifications. The problem is if there
are two outstanding notifications at time A and we call getnotify(),
they both get delivered to the client at the same time, but then only
one gets processed. The client will block until there's another
notification, but then it will process the second notification from time
A.
I've attached a patch which adds test_notify_twice() to demonstrate the
problem and puts getnotify() et al in a loop to address it.
It just occurred to me that you can have a similar problem if the
callback function uses the database connection to run a query, since the
notification can be delivered along with the query payload. Fixing that
would probably involve exposing PQnotifies() and calling it before
calling select(). I'm not contemplating doing this.
I haven't tested this with python 3, but it's based on the latest SVN
code as of last night.
--
Patrick TJ McPhee <[email protected]>
Index: pg.py
===================================================================
--- pg.py (revision 617)
+++ pg.py (working copy)
@@ -220,6 +223,7 @@
self.listen()
_ilist = [self.db.fileno()]
+ stopit = False
while True:
ilist, _olist, _elist = select.select(_ilist, [], [], self.timeout)
if ilist == []: # we timed out
@@ -228,24 +232,27 @@
break
else:
notice = self.db.getnotify()
- if notice is None:
- continue
- event, pid, extra = notice
- if event in (self.event, self.stop_event):
- self.arg_dict['pid'] = pid
- self.arg_dict['event'] = event
- self.arg_dict['extra'] = extra
- self.callback(self.arg_dict)
- if event == self.stop_event:
+ while notice:
+ event, pid, extra = notice
+ if event in (self.event, self.stop_event):
+ self.arg_dict['pid'] = pid
+ self.arg_dict['event'] = event
+ self.arg_dict['extra'] = extra
+ self.callback(self.arg_dict)
+ if event == self.stop_event:
+ self.unlisten()
+ stopit = True
+ break
+ else:
self.unlisten()
- break
- else:
- self.unlisten()
- raise _db_error(
- 'listening for "%s" and "%s", but notified of "%s"'
- % (self.event, self.stop_event, event))
+ raise _db_error(
+ 'listening for "%s" and "%s", but notified of "%s"'
+ % (self.event, self.stop_event, event))
+ notice = self.db.getnotify()
+ if stopit: break
+
def pgnotify(*args, **kw):
"""Same as NotificationHandler, under the traditional name."""
warnings.warn("pgnotify is deprecated, use NotificationHandler instead.",
Index: TEST_PyGreSQL_classic.py
===================================================================
--- TEST_PyGreSQL_classic.py (revision 617)
+++ TEST_PyGreSQL_classic.py (working copy)
@@ -248,7 +248,7 @@
else:
self.notify_timeout = True
- def test_notify(self):
+ def test_notify(self, two_payloads = False):
for run_as_method in False, True:
for call_notify in False, True:
db = opendb()
@@ -271,10 +271,18 @@
# Open another connection for sending notifications.
db2 = opendb()
# Generate notification from the other connection.
+ if two_payloads:
+ db2.begin()
if call_notify:
+ if two_payloads:
+ target.notify(db2, payload='payload 0')
target.notify(db2, payload='payload 1')
else:
+ if two_payloads:
+ db2.query("notify event_1, 'payload 0'")
db2.query("notify event_1, 'payload 1'")
+ if two_payloads:
+ db2.commit()
# Wait until the notification has been caught.
for n in range(500):
if arg_dict['called'] or self.notify_timeout:
@@ -310,6 +318,9 @@
self.assertFalse(target.listening)
target.close()
+ def test_notify_twice(self):
+ self.test_notify(True)
+
def test_notify_timeout(self):
for run_as_method in False, True:
db = opendb()
_______________________________________________
PyGreSQL mailing list
[email protected]
https://mail.vex.net/mailman/listinfo.cgi/pygresql