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  __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',              # warns of impending maintenance and asks for disconnect 
 24          u'db_maintenance_disconnect'    # announces a forced disconnect and disconnects 
 25  ] 
 26  #===================================================================== 
27 -class gmBackendListener(gmBorg.cBorg):
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 # the listener thread will regularly try to acquire 40 # this lock, when it succeeds it will quit 41 self._quit_lock = threading.Lock() 42 # take the lock now so it cannot be taken by the worker 43 # thread until it is released in shutdown() 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) # autocommit mode 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() # lock for access to connection object 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 # check for messages every 'poll_interval' seconds 66 self._poll_interval = poll_interval 67 self._listener_thread = None 68 self.__start_thread() 69 70 self.already_inited = True
71 #------------------------------- 72 # public API 73 #-------------------------------
74 - def shutdown(self):
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 # give the worker thread time to terminate 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 # event handlers 109 #-------------------------------
110 - def _on_pre_patient_selection(self, *args, **kwargs):
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 # internal helpers 119 #-------------------------------
120 - def __register_interests(self):
121 122 # determine patient-specific notifications 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 # determine unspecific notifications 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 # listen to patient changes inside the local client 150 # so we can re-register patient specific notifications 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 # do we need to start listening to patient specific 155 # notifications right away because we missed an 156 # earlier patient activation ? 157 self.__register_patient_notifications() 158 159 # listen to unspecific (non-patient related) notifications 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 #-------------------------------
210 - def __shutdown_connection(self):
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 #-------------------------------
219 - def __start_thread(self):
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 # the actual thread code 232 #-------------------------------
233 - def _process_notifications(self):
234 235 # get a cursor for this thread 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 # loop until quitting 243 _have_quit_lock = None 244 while not _have_quit_lock: 245 246 # quitting ? 247 if self._quit_lock.acquire(0): 248 break 249 250 # wait at most self._poll_interval for new data 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 # any input available ? 258 if len(ready_input_sockets) == 0: 259 # no, select.select() timed out 260 # give others a chance to grab the conn lock (eg listen/unlisten) 261 time.sleep(0.3) 262 continue 263 264 # data available, wait for it to fully arrive 265 # while not self._cursor.isready(): 266 # pass 267 # replace by conn.poll() when psycopg2 2.2 becomes standard 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 # any notifications ? 276 while len(self._conn.notifies) > 0: 277 # if self._quit_lock can be acquired we may be in 278 # __del__ in which case gmDispatcher is not 279 # guarantueed to exist anymore 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 # try sending intra-client signal 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 # there *may* be more pending notifications but do we care ? 305 if self._quit_lock.acquire(0): 306 _have_quit_lock = 1 307 break 308 309 # exit thread activity 310 return
311 #===================================================================== 312 # main 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 #-------------------------------
331 - def run_test():
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 # try loop without backend listener 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 # now try with listener to measure impact 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 #-------------------------------
399 - def run_monitor():
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