1 """GNUmed database backend listener.
2
3 This module implements threaded listening for asynchronuous
4 notifications from the database backend.
5 """
6
7 __version__ = "$Revision: 1.22 $"
8 __author__ = "H. Herb <hherb@gnumed.net>, K.Hilbert <karsten.hilbert@gmx.net>"
9
10 import sys, time, threading, select, logging
11
12
13 if __name__ == '__main__':
14 sys.path.insert(0, '../../')
15 from Gnumed.pycommon import gmDispatcher, gmExceptions, gmBorg
16
17
18 _log = logging.getLogger('gm.db')
19 _log.info(__version__)
20
21
22 static_signals = [
23 u'db_maintenance_warning',
24 u'db_maintenance_disconnect'
25 ]
26
28
29 - def __init__(self, conn=None, poll_interval=3, patient=None):
30
31 try:
32 self.already_inited
33 return
34 except AttributeError:
35 pass
36
37 _log.info('starting backend notifications listener thread')
38
39
40
41 self._quit_lock = threading.Lock()
42
43
44 if not self._quit_lock.acquire(0):
45 _log.error('cannot acquire thread-quit lock ! aborting')
46 raise gmExceptions.ConstructorError, "cannot acquire thread-quit lock"
47
48 self._conn = conn
49 self.backend_pid = self._conn.get_backend_pid()
50 _log.debug('connection has backend PID [%s]', self.backend_pid)
51 self._conn.set_isolation_level(0)
52 self._cursor = self._conn.cursor()
53 try:
54 self._conn_fd = self._conn.fileno()
55 except AttributeError:
56 self._conn_fd = self._cursor.fileno()
57 self._conn_lock = threading.Lock()
58
59 self.curr_patient_pk = None
60 if patient is not None:
61 if patient.connected:
62 self.curr_patient_pk = patient.ID
63 self.__register_interests()
64
65
66 self._poll_interval = poll_interval
67 self._listener_thread = None
68 self.__start_thread()
69
70 self.already_inited = True
71
72
73
75 if self._listener_thread is None:
76 self.__shutdown_connection()
77 return
78
79 _log.info('stopping backend notifications listener thread')
80 self._quit_lock.release()
81 try:
82
83 self._listener_thread.join(self._poll_interval+2.0)
84 try:
85 if self._listener_thread.isAlive():
86 _log.error('listener thread still alive after join()')
87 _log.debug('active threads: %s' % threading.enumerate())
88 except:
89 pass
90 except:
91 print sys.exc_info()
92
93 self._listener_thread = None
94
95 try:
96 self.__unregister_patient_notifications()
97 except:
98 _log.exception('unable to unregister patient notifications')
99 try:
100 self.__unregister_unspecific_notifications()
101 except:
102 _log.exception('unable to unregister unspecific notifications')
103
104 self.__shutdown_connection()
105
106 return
107
108
109
111 self.__unregister_patient_notifications()
112 self.curr_patient_pk = None
113
114 - def _on_post_patient_selection(self, *args, **kwargs):
115 self.curr_patient_pk = kwargs['pk_identity']
116 self.__register_patient_notifications()
117
118
119
121
122
123 cmd = u'SELECT DISTINCT ON (signal) signal FROM gm.notifying_tables WHERE carries_identity_pk IS true'
124 self._conn_lock.acquire(1)
125 try:
126 self._cursor.execute(cmd)
127 finally:
128 self._conn_lock.release()
129 rows = self._cursor.fetchall()
130 self.patient_specific_notifications = [ '%s_mod_db' % row[0] for row in rows ]
131 _log.info('configured patient specific notifications:')
132 _log.info('%s' % self.patient_specific_notifications)
133 gmDispatcher.known_signals.extend(self.patient_specific_notifications)
134
135
136 cmd = u'select distinct on (signal) signal from gm.notifying_tables where carries_identity_pk is False'
137 self._conn_lock.acquire(1)
138 try:
139 self._cursor.execute(cmd)
140 finally:
141 self._conn_lock.release()
142 rows = self._cursor.fetchall()
143 self.unspecific_notifications = [ '%s_mod_db' % row[0] for row in rows ]
144 self.unspecific_notifications.extend(static_signals)
145 _log.info('configured unspecific notifications:')
146 _log.info('%s' % self.unspecific_notifications)
147 gmDispatcher.known_signals.extend(self.unspecific_notifications)
148
149
150
151 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
152 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
153
154
155
156
157 self.__register_patient_notifications()
158
159
160 self.__register_unspecific_notifications()
161
163 if self.curr_patient_pk is None:
164 return
165 for notification in self.patient_specific_notifications:
166 notification = '%s:%s' % (notification, self.curr_patient_pk)
167 _log.debug('starting to listen for [%s]' % notification)
168 cmd = 'LISTEN "%s"' % notification
169 self._conn_lock.acquire(1)
170 try:
171 self._cursor.execute(cmd)
172 finally:
173 self._conn_lock.release()
174
176 if self.curr_patient_pk is None:
177 return
178 for notification in self.patient_specific_notifications:
179 notification = '%s:%s' % (notification, self.curr_patient_pk)
180 _log.debug('stopping to listen for [%s]' % notification)
181 cmd = 'UNLISTEN "%s"' % notification
182 self._conn_lock.acquire(1)
183 try:
184 self._cursor.execute(cmd)
185 finally:
186 self._conn_lock.release()
187
189 for sig in self.unspecific_notifications:
190 sig = '%s:' % sig
191 _log.info('starting to listen for [%s]' % sig)
192 cmd = 'LISTEN "%s"' % sig
193 self._conn_lock.acquire(1)
194 try:
195 self._cursor.execute(cmd)
196 finally:
197 self._conn_lock.release()
198
200 for sig in self.unspecific_notifications:
201 sig = '%s:' % sig
202 _log.info('stopping to listen for [%s]' % sig)
203 cmd = 'UNLISTEN "%s"' % sig
204 self._conn_lock.acquire(1)
205 try:
206 self._cursor.execute(cmd)
207 finally:
208 self._conn_lock.release()
209
211 _log.debug('shutting down connection with backend PID [%s]', self.backend_pid)
212 self._conn_lock.acquire(1)
213 try:
214 self._conn.rollback()
215 self._conn.close()
216 finally:
217 self._conn_lock.release()
218
220 if self._conn is None:
221 raise ValueError("no connection to backend available, useless to start thread")
222
223 self._listener_thread = threading.Thread (
224 target = self._process_notifications,
225 name = self.__class__.__name__
226 )
227 self._listener_thread.setDaemon(True)
228 _log.info('starting listener thread')
229 self._listener_thread.start()
230
231
232
234
235
236 self._conn_lock.acquire(1)
237 try:
238 self._cursor_in_thread = self._conn.cursor()
239 finally:
240 self._conn_lock.release()
241
242
243 _have_quit_lock = None
244 while not _have_quit_lock:
245
246
247 if self._quit_lock.acquire(0):
248 break
249
250
251 self._conn_lock.acquire(1)
252 try:
253 ready_input_sockets = select.select([self._conn_fd], [], [], self._poll_interval)[0]
254 finally:
255 self._conn_lock.release()
256
257
258 if len(ready_input_sockets) == 0:
259
260
261 time.sleep(0.3)
262 continue
263
264
265
266
267
268 self._conn_lock.acquire(1)
269 try:
270 self._cursor_in_thread.execute(u'SELECT 1')
271 self._cursor_in_thread.fetchall()
272 finally:
273 self._conn_lock.release()
274
275
276 while len(self._conn.notifies) > 0:
277
278
279
280 if self._quit_lock.acquire(0):
281 _have_quit_lock = 1
282 break
283
284 self._conn_lock.acquire(1)
285 try:
286 notification = self._conn.notifies.pop()
287 finally:
288 self._conn_lock.release()
289
290 pid, full_signal = notification
291 signal_name, pk = full_signal.split(':')
292 try:
293 results = gmDispatcher.send (
294 signal = signal_name,
295 originated_in_database = True,
296 listener_pid = self.backend_pid,
297 sending_backend_pid = pid,
298 pk_identity = pk
299 )
300 except:
301 print "problem routing notification [%s] from backend [%s] to intra-client dispatcher" % (full_signal, pid)
302 print sys.exc_info()
303
304
305 if self._quit_lock.acquire(0):
306 _have_quit_lock = 1
307 break
308
309
310 return
311
312
313
314 if __name__ == "__main__":
315
316 if len(sys.argv) < 2:
317 sys.exit()
318
319 if sys.argv[1] not in ['test', 'monitor']:
320 sys.exit()
321
322
323 notifies = 0
324
325 from Gnumed.pycommon import gmPG2, gmI18N
326 from Gnumed.business import gmPerson
327
328 gmI18N.activate_locale()
329 gmI18N.install_domain(domain='gnumed')
330
332
333
334 def dummy(n):
335 return float(n)*n/float(1+n)
336
337 def OnPatientModified():
338 global notifies
339 notifies += 1
340 sys.stdout.flush()
341 print "\nBackend says: patient data has been modified (%s. notification)" % notifies
342
343 try:
344 n = int(sys.argv[2])
345 except:
346 print "You can set the number of iterations\nwith the second command line argument"
347 n = 100000
348
349
350 print "Looping", n, "times through dummy function"
351 i = 0
352 t1 = time.time()
353 while i < n:
354 r = dummy(i)
355 i += 1
356 t2 = time.time()
357 t_nothreads = t2-t1
358 print "Without backend thread, it took", t_nothreads, "seconds"
359
360 listener = gmBackendListener(conn = gmPG2.get_raw_connection())
361
362
363 print "Now in a new shell connect psql to the"
364 print "database <gnumed_v9> on localhost, return"
365 print "here and hit <enter> to continue."
366 raw_input('hit <enter> when done starting psql')
367 print "You now have about 30 seconds to go"
368 print "to the psql shell and type"
369 print " notify patient_changed<enter>"
370 print "several times."
371 print "This should trigger our backend listening callback."
372 print "You can also try to stop the demo with Ctrl-C !"
373
374 listener.register_callback('patient_changed', OnPatientModified)
375
376 try:
377 counter = 0
378 while counter < 20:
379 counter += 1
380 time.sleep(1)
381 sys.stdout.flush()
382 print '.',
383 print "Looping",n,"times through dummy function"
384 i = 0
385 t1 = time.time()
386 while i < n:
387 r = dummy(i)
388 i += 1
389 t2 = time.time()
390 t_threaded = t2-t1
391 print "With backend thread, it took", t_threaded, "seconds"
392 print "Difference:", t_threaded-t_nothreads
393 except KeyboardInterrupt:
394 print "cancelled by user"
395
396 listener.shutdown()
397 listener.unregister_callback('patient_changed', OnPatientModified)
398
400
401 print "starting up backend notifications monitor"
402
403 def monitoring_callback(*args, **kwargs):
404 try:
405 kwargs['originated_in_database']
406 print '==> got notification from database "%s":' % kwargs['signal']
407 except KeyError:
408 print '==> received signal from client: "%s"' % kwargs['signal']
409 del kwargs['signal']
410 for key in kwargs.keys():
411 print ' [%s]: %s' % (key, kwargs[key])
412
413 gmDispatcher.connect(receiver = monitoring_callback)
414
415 listener = gmBackendListener(conn = gmPG2.get_raw_connection())
416 print "listening for the following notifications:"
417 print "1) patient specific (patient #%s):" % listener.curr_patient_pk
418 for sig in listener.patient_specific_notifications:
419 print ' - %s' % sig
420 print "1) unspecific:"
421 for sig in listener.unspecific_notifications:
422 print ' - %s' % sig
423
424 while True:
425 pat = gmPerson.ask_for_patient()
426 if pat is None:
427 break
428 print "found patient", pat
429 gmPerson.set_active_patient(patient=pat)
430 print "now waiting for notifications, hit <ENTER> to select another patient"
431 raw_input()
432
433 print "cleanup"
434 listener.shutdown()
435
436 print "shutting down backend notifications monitor"
437
438
439 if sys.argv[1] == 'monitor':
440 run_monitor()
441 else:
442 run_test()
443
444
445