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