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 _have_quit_lock = None 235 while not _have_quit_lock: 236 if self._quit_lock.acquire(0): 237 break 238 # wait at most self._poll_interval for new data 239 self._conn_lock.acquire(1) 240 try: 241 ready_input_sockets = select.select([self._conn_fd], [], [], self._poll_interval)[0] 242 finally: 243 self._conn_lock.release() 244 # any input available ? 245 if len(ready_input_sockets) == 0: 246 # no, select.select() timed out 247 # give others a chance to grab the conn lock (eg listen/unlisten) 248 time.sleep(0.3) 249 continue 250 # data available, wait for it to fully arrive 251 while not self._cursor.isready(): 252 pass 253 # any notifications ? 254 while len(self._conn.notifies) > 0: 255 # if self._quit_lock can be acquired we may be in 256 # __del__ in which case gmDispatcher is not 257 # guarantueed to exist anymore 258 if self._quit_lock.acquire(0): 259 _have_quit_lock = 1 260 break 261 262 self._conn_lock.acquire(1) 263 try: 264 notification = self._conn.notifies.pop() 265 finally: 266 self._conn_lock.release() 267 # try sending intra-client signal 268 pid, full_signal = notification 269 signal_name, pk = full_signal.split(':') 270 try: 271 results = gmDispatcher.send ( 272 signal = signal_name, 273 originated_in_database = True, 274 listener_pid = self.backend_pid, 275 sending_backend_pid = pid, 276 pk_identity = pk 277 ) 278 except: 279 print "problem routing notification [%s] from backend [%s] to intra-client dispatcher" % (full_signal, pid) 280 print sys.exc_info() 281 282 # there *may* be more pending notifications but do we care ? 283 if self._quit_lock.acquire(0): 284 _have_quit_lock = 1 285 break 286 287 # exit thread activity 288 return
289 #===================================================================== 290 # main 291 #===================================================================== 292 if __name__ == "__main__": 293 294 if len(sys.argv) < 2: 295 sys.exit() 296 297 if sys.argv[1] not in ['test', 'monitor']: 298 sys.exit() 299 300 301 notifies = 0 302 303 from Gnumed.pycommon import gmPG2, gmI18N 304 from Gnumed.business import gmPerson 305 306 gmI18N.activate_locale() 307 gmI18N.install_domain(domain='gnumed') 308 #-------------------------------
309 - def run_test():
310 311 #------------------------------- 312 def dummy(n): 313 return float(n)*n/float(1+n)
314 #------------------------------- 315 def OnPatientModified(): 316 global notifies 317 notifies += 1 318 sys.stdout.flush() 319 print "\nBackend says: patient data has been modified (%s. notification)" % notifies 320 #------------------------------- 321 try: 322 n = int(sys.argv[2]) 323 except: 324 print "You can set the number of iterations\nwith the second command line argument" 325 n = 100000 326 327 # try loop without backend listener 328 print "Looping", n, "times through dummy function" 329 i = 0 330 t1 = time.time() 331 while i < n: 332 r = dummy(i) 333 i += 1 334 t2 = time.time() 335 t_nothreads = t2-t1 336 print "Without backend thread, it took", t_nothreads, "seconds" 337 338 listener = gmBackendListener(conn = gmPG2.get_raw_connection()) 339 340 # now try with listener to measure impact 341 print "Now in a new shell connect psql to the" 342 print "database <gnumed_v9> on localhost, return" 343 print "here and hit <enter> to continue." 344 raw_input('hit <enter> when done starting psql') 345 print "You now have about 30 seconds to go" 346 print "to the psql shell and type" 347 print " notify patient_changed<enter>" 348 print "several times." 349 print "This should trigger our backend listening callback." 350 print "You can also try to stop the demo with Ctrl-C !" 351 352 listener.register_callback('patient_changed', OnPatientModified) 353 354 try: 355 counter = 0 356 while counter < 20: 357 counter += 1 358 time.sleep(1) 359 sys.stdout.flush() 360 print '.', 361 print "Looping",n,"times through dummy function" 362 i = 0 363 t1 = time.time() 364 while i < n: 365 r = dummy(i) 366 i += 1 367 t2 = time.time() 368 t_threaded = t2-t1 369 print "With backend thread, it took", t_threaded, "seconds" 370 print "Difference:", t_threaded-t_nothreads 371 except KeyboardInterrupt: 372 print "cancelled by user" 373 374 listener.shutdown() 375 listener.unregister_callback('patient_changed', OnPatientModified) 376 #-------------------------------
377 - def run_monitor():
378 379 print "starting up backend notifications monitor" 380 381 def monitoring_callback(*args, **kwargs): 382 try: 383 kwargs['originated_in_database'] 384 print '==> got notification from database "%s":' % kwargs['signal'] 385 except KeyError: 386 print '==> received signal from client: "%s"' % kwargs['signal'] 387 del kwargs['signal'] 388 for key in kwargs.keys(): 389 print ' [%s]: %s' % (key, kwargs[key])
390 391 gmDispatcher.connect(receiver = monitoring_callback) 392 393 listener = gmBackendListener(conn = gmPG2.get_raw_connection()) 394 print "listening for the following notifications:" 395 print "1) patient specific (patient #%s):" % listener.curr_patient_pk 396 for sig in listener.patient_specific_notifications: 397 print ' - %s' % sig 398 print "1) unspecific:" 399 for sig in listener.unspecific_notifications: 400 print ' - %s' % sig 401 402 while True: 403 pat = gmPerson.ask_for_patient() 404 if pat is None: 405 break 406 print "found patient", pat 407 gmPerson.set_active_patient(patient=pat) 408 print "now waiting for notifications, hit <ENTER> to select another patient" 409 raw_input() 410 411 print "cleanup" 412 listener.shutdown() 413 414 print "shutting down backend notifications monitor" 415 416 #------------------------------- 417 if sys.argv[1] == 'monitor': 418 run_monitor() 419 else: 420 run_test() 421 422 #===================================================================== 423