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 finally:
217 self._conn_lock.release()
218
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
232
234 _have_quit_lock = None
235 while not _have_quit_lock:
236 if self._quit_lock.acquire(0):
237 break
238
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
245 if len(ready_input_sockets) == 0:
246
247
248 time.sleep(0.3)
249 continue
250
251 while not self._cursor.isready():
252 pass
253
254 while len(self._conn.notifies) > 0:
255
256
257
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
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
283 if self._quit_lock.acquire(0):
284 _have_quit_lock = 1
285 break
286
287
288 return
289
290
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
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
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
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
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