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',
24 u'db_maintenance_disconnect'
25 ]
26
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
40
41 self._quit_lock = threading.Lock()
42
43
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)
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()
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
66 self._poll_interval = poll_interval
67 self._listener_thread = None
68 self.__start_thread()
69
70 self.already_inited = True
71
72
73
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
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
109
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
119
121
122
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
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
150
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
155
156
157 self.__register_patient_notifications()
158
159
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
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 except:
217 pass
218 finally:
219 self._conn_lock.release()
220
222 if self._conn is None:
223 raise ValueError("no connection to backend available, useless to start thread")
224
225 self._listener_thread = threading.Thread (
226 target = self._process_notifications,
227 name = self.__class__.__name__
228 )
229 self._listener_thread.setDaemon(True)
230 _log.info('starting listener thread')
231 self._listener_thread.start()
232
233
234
236
237
238 self._conn_lock.acquire(1)
239 try:
240 self._cursor_in_thread = self._conn.cursor()
241 finally:
242 self._conn_lock.release()
243
244
245 _have_quit_lock = None
246 while not _have_quit_lock:
247
248
249 if self._quit_lock.acquire(0):
250 break
251
252
253 self._conn_lock.acquire(1)
254 try:
255 ready_input_sockets = select.select([self._conn_fd], [], [], self._poll_interval)[0]
256 finally:
257 self._conn_lock.release()
258
259
260 if len(ready_input_sockets) == 0:
261
262
263 time.sleep(0.3)
264 continue
265
266
267
268
269
270 self._conn_lock.acquire(1)
271 try:
272 self._cursor_in_thread.execute(u'SELECT 1')
273 self._cursor_in_thread.fetchall()
274 finally:
275 self._conn_lock.release()
276
277
278 while len(self._conn.notifies) > 0:
279
280
281
282 if self._quit_lock.acquire(0):
283 _have_quit_lock = 1
284 break
285
286 self._conn_lock.acquire(1)
287 try:
288 notification = self._conn.notifies.pop()
289 finally:
290 self._conn_lock.release()
291
292 pid, full_signal = notification
293 signal_name, pk = full_signal.split(':')
294 try:
295 results = gmDispatcher.send (
296 signal = signal_name,
297 originated_in_database = True,
298 listener_pid = self.backend_pid,
299 sending_backend_pid = pid,
300 pk_identity = pk
301 )
302 except:
303 print "problem routing notification [%s] from backend [%s] to intra-client dispatcher" % (full_signal, pid)
304 print sys.exc_info()
305
306
307 if self._quit_lock.acquire(0):
308 _have_quit_lock = 1
309 break
310
311
312 return
313
314
315
316 if __name__ == "__main__":
317
318 if len(sys.argv) < 2:
319 sys.exit()
320
321 if sys.argv[1] not in ['test', 'monitor']:
322 sys.exit()
323
324
325 notifies = 0
326
327 from Gnumed.pycommon import gmPG2, gmI18N
328 from Gnumed.business import gmPerson, gmPersonSearch
329
330 gmI18N.activate_locale()
331 gmI18N.install_domain(domain='gnumed')
332
334
335
336 def dummy(n):
337 return float(n)*n/float(1+n)
338
339 def OnPatientModified():
340 global notifies
341 notifies += 1
342 sys.stdout.flush()
343 print "\nBackend says: patient data has been modified (%s. notification)" % notifies
344
345 try:
346 n = int(sys.argv[2])
347 except:
348 print "You can set the number of iterations\nwith the second command line argument"
349 n = 100000
350
351
352 print "Looping", n, "times through dummy function"
353 i = 0
354 t1 = time.time()
355 while i < n:
356 r = dummy(i)
357 i += 1
358 t2 = time.time()
359 t_nothreads = t2-t1
360 print "Without backend thread, it took", t_nothreads, "seconds"
361
362 listener = gmBackendListener(conn = gmPG2.get_raw_connection())
363
364
365 print "Now in a new shell connect psql to the"
366 print "database <gnumed_v9> on localhost, return"
367 print "here and hit <enter> to continue."
368 raw_input('hit <enter> when done starting psql')
369 print "You now have about 30 seconds to go"
370 print "to the psql shell and type"
371 print " notify patient_changed<enter>"
372 print "several times."
373 print "This should trigger our backend listening callback."
374 print "You can also try to stop the demo with Ctrl-C !"
375
376 listener.register_callback('patient_changed', OnPatientModified)
377
378 try:
379 counter = 0
380 while counter < 20:
381 counter += 1
382 time.sleep(1)
383 sys.stdout.flush()
384 print '.',
385 print "Looping",n,"times through dummy function"
386 i = 0
387 t1 = time.time()
388 while i < n:
389 r = dummy(i)
390 i += 1
391 t2 = time.time()
392 t_threaded = t2-t1
393 print "With backend thread, it took", t_threaded, "seconds"
394 print "Difference:", t_threaded-t_nothreads
395 except KeyboardInterrupt:
396 print "cancelled by user"
397
398 listener.shutdown()
399 listener.unregister_callback('patient_changed', OnPatientModified)
400
402
403 print "starting up backend notifications monitor"
404
405 def monitoring_callback(*args, **kwargs):
406 try:
407 kwargs['originated_in_database']
408 print '==> got notification from database "%s":' % kwargs['signal']
409 except KeyError:
410 print '==> received signal from client: "%s"' % kwargs['signal']
411 del kwargs['signal']
412 for key in kwargs.keys():
413 print ' [%s]: %s' % (key, kwargs[key])
414
415 gmDispatcher.connect(receiver = monitoring_callback)
416
417 listener = gmBackendListener(conn = gmPG2.get_raw_connection())
418 print "listening for the following notifications:"
419 print "1) patient specific (patient #%s):" % listener.curr_patient_pk
420 for sig in listener.patient_specific_notifications:
421 print ' - %s' % sig
422 print "1) unspecific:"
423 for sig in listener.unspecific_notifications:
424 print ' - %s' % sig
425
426 while True:
427 pat = gmPersonSearch.ask_for_patient()
428 if pat is None:
429 break
430 print "found patient", pat
431 gmPerson.set_active_patient(patient=pat)
432 print "now waiting for notifications, hit <ENTER> to select another patient"
433 raw_input()
434
435 print "cleanup"
436 listener.shutdown()
437
438 print "shutting down backend notifications monitor"
439
440
441 if sys.argv[1] == 'monitor':
442 run_monitor()
443 else:
444 run_test()
445
446
447