Package Gnumed :: Package pycommon :: Module gmBackendListener
[frames] | no frames]

Source Code for Module Gnumed.pycommon.gmBackendListener

  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  #===================================================================== 
28 -class gmBackendListener(gmBorg.cBorg):
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 # 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 #-------------------------------
71 - def shutdown(self):
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 #-------------------------------
107 - def _on_pre_patient_selection(self, *args, **kwargs):
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 # internal helpers 116 #-------------------------------
117 - def __register_interests(self):
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 #-------------------------------
195 - def __shutdown_connection(self):
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 #-------------------------------
202 - def __start_thread(self):
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 #-------------------------------
216 - def _process_notifications(self):
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 #-------------------------------
281 - def run_test():
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 # 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 #-------------------------------
349 - def run_monitor():
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