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 self._conn_lock = threading.Lock()
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
62 self._poll_interval = poll_interval
63 self._listener_thread = None
64 self.__start_thread()
65
66 self.already_inited = True
67
68
69
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
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
105
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
115
117
118
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
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
146
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
151
152
153 self.__register_patient_notifications()
154
155
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
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
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
228
230 _have_quit_lock = None
231 while not _have_quit_lock:
232 if self._quit_lock.acquire(0):
233 break
234
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
241 if len(ready_input_sockets) == 0:
242
243
244 time.sleep(0.3)
245 continue
246
247 while not self._cursor.isready():
248 pass
249
250 while len(self._conn.notifies) > 0:
251
252
253
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
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
279 if self._quit_lock.acquire(0):
280 _have_quit_lock = 1
281 break
282
283
284 return
285
286
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
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
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
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
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