| Trees | Indices | Help |
|
|---|
|
|
1 """GNUmed database backend listener.
2
3 This module implements threaded listening for asynchronuous
4 notifications from the database backend.
5 """
6 #=====================================================================
7 # $Source: /cvsroot/gnumed/gnumed/gnumed/client/pycommon/gmBackendListener.py,v $
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', # warns of impending maintenance and asks for disconnect
25 u'db_maintenance_disconnect' # announces a forced disconnect and disconnects
26 ]
27 #=====================================================================
29
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 # the listener thread will regularly try to acquire
41 # this lock, when it succeeds it will quit
42 self._quit_lock = threading.Lock()
43 # take the lock now so it cannot be taken by the worker
44 # thread until it is released in shutdown()
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) # autocommit mode
53 self._cursor = self._conn.cursor()
54 self._conn_lock = threading.Lock() # lock for access to connection object
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 # check for messages every 'poll_interval' seconds
63 self._poll_interval = poll_interval
64 self._listener_thread = None
65 self.__start_thread()
66
67 self.already_inited = True
68 #-------------------------------
69 # public API
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 # give the worker thread time to terminate
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 # event handlers
106 #-------------------------------
110 #-------------------------------
114 #-------------------------------
115 # internal helpers
116 #-------------------------------
118
119 # determine patient-specific notifications
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 # determine unspecific notifications
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 # listen to patient changes inside the local client
143 # so we can re-register patient specific notifications
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 # do we need to start listening to patient specific
148 # notifications right away because we missed an
149 # earlier patient activation ?
150 self.__register_patient_notifications()
151
152 # listen to unspecific (non-patient related) notifications
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 # the actual thread code
215 #-------------------------------
217 _have_quit_lock = None
218 while not _have_quit_lock:
219 if self._quit_lock.acquire(0):
220 break
221 # wait at most self._poll_interval for new data
222 self._conn_lock.acquire(1)
223 ready_input_sockets = select.select([self._cursor], [], [], self._poll_interval)[0]
224 self._conn_lock.release()
225 # any input available ?
226 if len(ready_input_sockets) == 0:
227 # no, select.select() timed out
228 # give others a chance to grab the conn lock (eg listen/unlisten)
229 time.sleep(0.3)
230 continue
231 # data available, wait for it to fully arrive
232 while not self._cursor.isready():
233 pass
234 # any notifications ?
235 while len(self._conn.notifies) > 0:
236 # if self._quit_lock can be acquired we may be in
237 # __del__ in which case gmDispatcher is not
238 # guarantueed to exist anymore
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 # try sending intra-client signal
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 # there *may* be more pending notifications but do we care ?
262 if self._quit_lock.acquire(0):
263 _have_quit_lock = 1
264 break
265
266 # exit thread activity
267 return
268 #=====================================================================
269 # main
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 #-------------------------------
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 # try loop without backend listener
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 # now try with listener to measure impact
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 # $Log: gmBackendListener.py,v $
396 # Revision 1.22 2009/07/02 20:47:34 ncq
397 # - stop-thread -> shutdown
398 # - properly shutdown connection
399 #
400 # Revision 1.21 2009/02/12 16:21:15 ncq
401 # - be more careful about signal de-registration
402 #
403 # Revision 1.20 2009/01/21 18:53:04 ncq
404 # - adjust to signals
405 #
406 # Revision 1.19 2008/11/20 18:43:01 ncq
407 # - better logger name
408 #
409 # Revision 1.18 2008/07/07 13:39:47 ncq
410 # - current patient .connected
411 #
412 # Revision 1.17 2008/06/15 20:17:17 ncq
413 # - be even more careful rejoining worker threads
414 #
415 # Revision 1.16 2008/04/28 13:31:16 ncq
416 # - now static signals for database maintenance
417 #
418 # Revision 1.15 2008/01/07 19:48:22 ncq
419 # - bump db version
420 #
421 # Revision 1.14 2007/12/12 16:17:15 ncq
422 # - better logger names
423 #
424 # Revision 1.13 2007/12/11 14:16:29 ncq
425 # - cleanup
426 # - use logging
427 #
428 # Revision 1.12 2007/10/30 12:48:17 ncq
429 # - attach_identity_pk -> carries_identity_pk
430 #
431 # Revision 1.11 2007/10/25 12:18:37 ncq
432 # - cleanup
433 # - include listener backend pid in signal data
434 #
435 # Revision 1.10 2007/10/23 21:22:42 ncq
436 # - completely redone:
437 # - use psycopg2
438 # - handle signals based on backend metadata
439 # - add monitor to test cases
440 #
441 # Revision 1.9 2006/05/24 12:50:21 ncq
442 # - now only empty string '' means use local UNIX domain socket connections
443 #
444 # Revision 1.8 2005/01/27 17:23:14 ncq
445 # - just some cleanup
446 #
447 # Revision 1.7 2005/01/12 14:47:48 ncq
448 # - in DB speak the database owner is customarily called dbo, hence use that
449 #
450 # Revision 1.6 2004/06/25 12:28:25 ncq
451 # - just cleanup
452 #
453 # Revision 1.5 2004/06/15 19:18:06 ncq
454 # - _unlisten_notification() now accepts a list of notifications to unlisten from
455 # - cleanup/enhance __del__
456 # - slightly untighten notification handling loop so others
457 # get a chance to grab the connection lock
458 #
459 # Revision 1.4 2004/06/09 14:42:05 ncq
460 # - cleanup, clarification
461 # - improve exception handling in __del__
462 # - tell_thread_to_stop() -> stop_thread(), uses self._listener_thread.join()
463 # now, hence may take at max self._poll_interval+2 seconds longer but is
464 # considerably cleaner/safer
465 # - vastly simplify threaded notification handling loop
466 #
467 # Revision 1.3 2004/06/01 23:42:53 ncq
468 # - improve error message from failed notify dispatch attempt
469 #
470 # Revision 1.2 2004/04/21 14:27:15 ihaywood
471 # bug preventing backendlistener working on local socket connections
472 #
473 # Revision 1.1 2004/02/25 09:30:13 ncq
474 # - moved here from python-common
475 #
476 # Revision 1.21 2004/01/18 21:45:50 ncq
477 # - use real lock for thread quit indicator
478 #
479 # Revision 1.20 2003/11/17 10:56:35 sjtan
480 #
481 # synced and commiting.
482 #
483 # Revision 1.1 2003/10/23 06:02:38 sjtan
484 #
485 # manual edit areas modelled after r.terry's specs.
486 #
487 # Revision 1.19 2003/09/11 10:53:10 ncq
488 # - fix test code in __main__
489 #
490 # Revision 1.18 2003/07/04 20:01:48 ncq
491 # - remove blocking keyword from acquire() since Python does not like the
492 #
493 # Revision 1.17 2003/06/26 04:18:40 ihaywood
494 # Fixes to gmCfg for commas
495 #
496 # Revision 1.16 2003/06/03 13:21:20 ncq
497 # - still some problems syncing with threads on __del__ when
498 # failing in a constructor that sets up threads also
499 # - slightly better comments in threaded code
500 #
501 # Revision 1.15 2003/06/01 12:55:58 sjtan
502 #
503 # sql commit may cause PortalClose, whilst connection.commit() doesnt?
504 #
505 # Revision 1.14 2003/05/27 15:23:48 ncq
506 # - Sian found a uncleanliness in releasing the lock
507 # during notification registration, clean up his fix
508 #
509 # Revision 1.13 2003/05/27 14:38:22 sjtan
510 #
511 # looks like was intended to be caught if throws exception here.
512 #
513 # Revision 1.12 2003/05/03 14:14:27 ncq
514 # - slightly better variable names
515 # - keep reference to thread so we properly rejoin() upon __del__
516 # - helper __unlisten_signal()
517 # - remove notification from list of intercepted ones upon unregister_callback
518 # - be even more careful in thread such that to stop quickly
519 #
520 # Revision 1.11 2003/05/03 00:42:11 ncq
521 # - first shot at syncing thread at __del__ time, non-working
522 #
523 # Revision 1.10 2003/04/28 21:38:13 ncq
524 # - properly lock access to self._conn across threads
525 # - give others a chance to acquire the lock
526 #
527 # Revision 1.9 2003/04/27 11:34:02 ncq
528 # - rewrite register_callback() to allow for more than one callback per signal
529 # - add unregister_callback()
530 # - clean up __connect(), __start_thread()
531 # - optimize _process_notifications()
532 #
533 # Revision 1.8 2003/04/25 13:00:43 ncq
534 # - more cleanup/renaming on the way to more goodness, eventually
535 #
536 # Revision 1.7 2003/02/07 14:22:35 ncq
537 # - whitespace fix
538 #
539 # Revision 1.6 2003/01/16 14:45:03 ncq
540 # - debianized
541 #
542 # Revision 1.5 2002/09/26 13:21:37 ncq
543 # - log version
544 #
545 # Revision 1.4 2002/09/08 21:22:36 ncq
546 # - removed one debugging level print()
547 #
548 # Revision 1.3 2002/09/08 20:58:46 ncq
549 # - made comments more useful
550 # - added some more metadata to get in line with GnuMed coding standards
551 #
552
| Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Tue Feb 9 04:01:21 2010 | http://epydoc.sourceforge.net |