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 self._conn_lock = threading.Lock() # lock for access to connection object 54 55 self.curr_patient_pk = None 56 if patient is not None: 57 if patient.connected: 58 self.curr_patient_pk = patient.ID 59 self.__register_interests() 60 61 # check for messages every 'poll_interval' seconds 62 self._poll_interval = poll_interval 63 self._listener_thread = None 64 self.__start_thread() 65 66 self.already_inited = True
67 #------------------------------- 68 # public API 69 #-------------------------------
70 - def shutdown(self):
71 if self._listener_thread is None: 72 self.__shutdown_connection() 73 return 74 75 _log.info('stopping backend notifications listener thread') 76 self._quit_lock.release() 77 try: 78 # give the worker thread time to terminate 79 self._listener_thread.join(self._poll_interval+2.0) 80 try: 81 if self._listener_thread.isAlive(): 82 _log.error('listener thread still alive after join()') 83 _log.debug('active threads: %s' % threading.enumerate()) 84 except: 85 pass 86 except: 87 print sys.exc_info() 88 89 self._listener_thread = None 90 91 try: 92 self.__unregister_patient_notifications() 93 except: 94 _log.exception('unable to unregister patient notifications') 95 try: 96 self.__unregister_unspecific_notifications() 97 except: 98 _log.exception('unable to unregister unspecific notifications') 99 100 self.__shutdown_connection() 101 102 return
103 #------------------------------- 104 # event handlers 105 #-------------------------------
106 - def _on_pre_patient_selection(self, *args, **kwargs):
107 self.__unregister_patient_notifications() 108 self.curr_patient_pk = None
109 #-------------------------------
110 - def _on_post_patient_selection(self, *args, **kwargs):
111 self.curr_patient_pk = kwargs['pk_identity'] 112 self.__register_patient_notifications()
113 #------------------------------- 114 # internal helpers 115 #-------------------------------
116 - def __register_interests(self):
117 118 # determine patient-specific notifications 119 cmd = u'select distinct on (signal) signal from gm.notifying_tables where carries_identity_pk is True' 120 self._conn_lock.acquire(1) 121 try: 122 self._cursor.execute(cmd) 123 finally: 124 self._conn_lock.release() 125 rows = self._cursor.fetchall() 126 self.patient_specific_notifications = [ '%s_mod_db' % row[0] for row in rows ] 127 _log.info('configured patient specific notifications:') 128 _log.info('%s' % self.patient_specific_notifications) 129 gmDispatcher.known_signals.extend(self.patient_specific_notifications) 130 131 # determine unspecific notifications 132 cmd = u'select distinct on (signal) signal from gm.notifying_tables where carries_identity_pk is False' 133 self._conn_lock.acquire(1) 134 try: 135 self._cursor.execute(cmd) 136 finally: 137 self._conn_lock.release() 138 rows = self._cursor.fetchall() 139 self.unspecific_notifications = [ '%s_mod_db' % row[0] for row in rows ] 140 self.unspecific_notifications.extend(static_signals) 141 _log.info('configured unspecific notifications:') 142 _log.info('%s' % self.unspecific_notifications) 143 gmDispatcher.known_signals.extend(self.unspecific_notifications) 144 145 # listen to patient changes inside the local client 146 # so we can re-register patient specific notifications 147 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection) 148 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection) 149 150 # do we need to start listening to patient specific 151 # notifications right away because we missed an 152 # earlier patient activation ? 153 self.__register_patient_notifications() 154 155 # listen to unspecific (non-patient related) notifications 156 self.__register_unspecific_notifications()
157 #-------------------------------
159 if self.curr_patient_pk is None: 160 return 161 for notification in self.patient_specific_notifications: 162 notification = '%s:%s' % (notification, self.curr_patient_pk) 163 _log.debug('starting to listen for [%s]' % notification) 164 cmd = 'LISTEN "%s"' % notification 165 self._conn_lock.acquire(1) 166 try: 167 self._cursor.execute(cmd) 168 finally: 169 self._conn_lock.release()
170 #-------------------------------
172 if self.curr_patient_pk is None: 173 return 174 for notification in self.patient_specific_notifications: 175 notification = '%s:%s' % (notification, self.curr_patient_pk) 176 _log.debug('stopping to listen for [%s]' % notification) 177 cmd = 'UNLISTEN "%s"' % notification 178 self._conn_lock.acquire(1) 179 try: 180 self._cursor.execute(cmd) 181 finally: 182 self._conn_lock.release()
183 #-------------------------------
185 for sig in self.unspecific_notifications: 186 sig = '%s:' % sig 187 _log.info('starting to listen for [%s]' % sig) 188 cmd = 'LISTEN "%s"' % sig 189 self._conn_lock.acquire(1) 190 try: 191 self._cursor.execute(cmd) 192 finally: 193 self._conn_lock.release()
194 #-------------------------------
196 for sig in self.unspecific_notifications: 197 sig = '%s:' % sig 198 _log.info('stopping to listen for [%s]' % sig) 199 cmd = 'UNLISTEN "%s"' % sig 200 self._conn_lock.acquire(1) 201 try: 202 self._cursor.execute(cmd) 203 finally: 204 self._conn_lock.release()
205 #-------------------------------
206 - def __shutdown_connection(self):
207 _log.debug('shutting down connection with backend PID [%s]', self.backend_pid) 208 self._conn_lock.acquire(1) 209 try: 210 self._conn.rollback() 211 self._conn.close() 212 finally: 213 self._conn_lock.release()
214 #-------------------------------
215 - def __start_thread(self):
216 if self._conn is None: 217 raise ValueError("no connection to backend available, useless to start thread") 218 219 self._listener_thread = threading.Thread ( 220 target = self._process_notifications, 221 name = self.__class__.__name__ 222 ) 223 self._listener_thread.setDaemon(True) 224 _log.info('starting listener thread') 225 self._listener_thread.start()
226 #------------------------------- 227 # the actual thread code 228 #-------------------------------
229 - def _process_notifications(self):
230 _have_quit_lock = None 231 while not _have_quit_lock: 232 if self._quit_lock.acquire(0): 233 break 234 # wait at most self._poll_interval for new data 235 self._conn_lock.acquire(1) 236 try: 237 ready_input_sockets = select.select([self._cursor], [], [], self._poll_interval)[0] 238 finally: 239 self._conn_lock.release() 240 # any input available ? 241 if len(ready_input_sockets) == 0: 242 # no, select.select() timed out 243 # give others a chance to grab the conn lock (eg listen/unlisten) 244 time.sleep(0.3) 245 continue 246 # data available, wait for it to fully arrive 247 while not self._cursor.isready(): 248 pass 249 # any notifications ? 250 while len(self._conn.notifies) > 0: 251 # if self._quit_lock can be acquired we may be in 252 # __del__ in which case gmDispatcher is not 253 # guarantueed to exist anymore 254 if self._quit_lock.acquire(0): 255 _have_quit_lock = 1 256 break 257 258 self._conn_lock.acquire(1) 259 try: 260 notification = self._conn.notifies.pop() 261 finally: 262 self._conn_lock.release() 263 # try sending intra-client signal 264 pid, full_signal = notification 265 signal_name, pk = full_signal.split(':') 266 try: 267 results = gmDispatcher.send ( 268 signal = signal_name, 269 originated_in_database = True, 270 listener_pid = self.backend_pid, 271 sending_backend_pid = pid, 272 pk_identity = pk 273 ) 274 except: 275 print "problem routing notification [%s] from backend [%s] to intra-client dispatcher" % (full_signal, pid) 276 print sys.exc_info() 277 278 # there *may* be more pending notifications but do we care ? 279 if self._quit_lock.acquire(0): 280 _have_quit_lock = 1 281 break 282 283 # exit thread activity 284 return
285 #===================================================================== 286 # main 287 #===================================================================== 288 if __name__ == "__main__": 289 290 if len(sys.argv) < 2: 291 sys.exit() 292 293 if sys.argv[1] not in ['test', 'monitor']: 294 sys.exit() 295 296 297 notifies = 0 298 299 from Gnumed.pycommon import gmPG2, gmI18N 300 from Gnumed.business import gmPerson 301 302 gmI18N.activate_locale() 303 gmI18N.install_domain(domain='gnumed') 304 #-------------------------------
305 - def run_test():
306 307 #------------------------------- 308 def dummy(n): 309 return float(n)*n/float(1+n)
310 #------------------------------- 311 def OnPatientModified(): 312 global notifies 313 notifies += 1 314 sys.stdout.flush() 315 print "\nBackend says: patient data has been modified (%s. notification)" % notifies 316 #------------------------------- 317 try: 318 n = int(sys.argv[2]) 319 except: 320 print "You can set the number of iterations\nwith the second command line argument" 321 n = 100000 322 323 # try loop without backend listener 324 print "Looping", n, "times through dummy function" 325 i = 0 326 t1 = time.time() 327 while i < n: 328 r = dummy(i) 329 i += 1 330 t2 = time.time() 331 t_nothreads = t2-t1 332 print "Without backend thread, it took", t_nothreads, "seconds" 333 334 listener = gmBackendListener(conn = gmPG2.get_raw_connection()) 335 336 # now try with listener to measure impact 337 print "Now in a new shell connect psql to the" 338 print "database <gnumed_v9> on localhost, return" 339 print "here and hit <enter> to continue." 340 raw_input('hit <enter> when done starting psql') 341 print "You now have about 30 seconds to go" 342 print "to the psql shell and type" 343 print " notify patient_changed<enter>" 344 print "several times." 345 print "This should trigger our backend listening callback." 346 print "You can also try to stop the demo with Ctrl-C !" 347 348 listener.register_callback('patient_changed', OnPatientModified) 349 350 try: 351 counter = 0 352 while counter < 20: 353 counter += 1 354 time.sleep(1) 355 sys.stdout.flush() 356 print '.', 357 print "Looping",n,"times through dummy function" 358 i = 0 359 t1 = time.time() 360 while i < n: 361 r = dummy(i) 362 i += 1 363 t2 = time.time() 364 t_threaded = t2-t1 365 print "With backend thread, it took", t_threaded, "seconds" 366 print "Difference:", t_threaded-t_nothreads 367 except KeyboardInterrupt: 368 print "cancelled by user" 369 370 listener.shutdown() 371 listener.unregister_callback('patient_changed', OnPatientModified) 372 #-------------------------------
373 - def run_monitor():
374 375 print "starting up backend notifications monitor" 376 377 def monitoring_callback(*args, **kwargs): 378 try: 379 kwargs['originated_in_database'] 380 print '==> got notification from database "%s":' % kwargs['signal'] 381 except KeyError: 382 print '==> received signal from client: "%s"' % kwargs['signal'] 383 del kwargs['signal'] 384 for key in kwargs.keys(): 385 print ' [%s]: %s' % (key, kwargs[key])
386 387 gmDispatcher.connect(receiver = monitoring_callback) 388 389 listener = gmBackendListener(conn = gmPG2.get_raw_connection()) 390 print "listening for the following notifications:" 391 print "1) patient specific (patient #%s):" % listener.curr_patient_pk 392 for sig in listener.patient_specific_notifications: 393 print ' - %s' % sig 394 print "1) unspecific:" 395 for sig in listener.unspecific_notifications: 396 print ' - %s' % sig 397 398 while True: 399 pat = gmPerson.ask_for_patient() 400 if pat is None: 401 break 402 print "found patient", pat 403 gmPerson.set_active_patient(patient=pat) 404 print "now waiting for notifications, hit <ENTER> to select another patient" 405 raw_input() 406 407 print "cleanup" 408 listener.shutdown() 409 410 print "shutting down backend notifications monitor" 411 412 #------------------------------- 413 if sys.argv[1] == 'monitor': 414 run_monitor() 415 else: 416 run_test() 417 418 #===================================================================== 419