Handling concurrent database modifications in GNUmed

A design principle in GNUmed is to block concurrent use of patient data as little as possible. Most of the time this is no problem because different clients work on different patients and the design of PostgreSQL easily allows for that.

However, every so often, the same patient will be open and edited on several workplaces throughout a practice. There are two problems that need to be taken care of in this scenario:

1) Propagation of changes to a patient record displayed on several machines

Assume this scenario as an example:

  • reception nurse elicits report of allergy from patient
  • patient moves to exam room
  • patient record opened in exam room
  • nurse gets around to record allergy at front desk

The problem arises because the exam room machine read the patient record before the allergy was entered such that no allergy is being displayed to the doctor. While the doctor is, of course, medico-legally required to ask for allergies before starting a treatment, it is desirable that the exam room frontend be notified about the new allergy having been entered into the system without dependence on the downstream user to check whether the information had undergone any change.

To enable this notification GNUmed uses the NOTIFY/LISTEN facility of PostgreSQL.

Backend

During database creation the allergy table is configured to deliver signals to interested frontends:
   select gm.add_table_for_notifies('clin', 'allergy', 'allg');
There are several flavours of add_table_for_notifies() available and documented in the gm schema. Metadata for tables sending out notifications is stored in gm.notifying_tables. The last (boolean) parameter defines whether the notification system will .

Later in the bootstrapping process the Python script gmNotificationSchemaGenerator.py creates appropriate AFTER UPDATE/INSERT/DELETE triggers based on the information in gm.notifying_tables. If the respective table contains a column known to directly or indirectly link to a patient identity the trigger will add the respective patient primary key to the signal it sends out - as opposed to, say, lookup tables.

During normal database operation those triggers send out a NOTIFY whenever an UPDATE/INSERT/DELETE commits from any client. In the above example the signal name eventually sent out would be allg_mod_db:123 assuming a row related to patient 123 was changed.

Frontend

During client startup a thread is created which listens for NOTIFY signals from the database. It autoconfigures itself to listen for all signals recorded in gm.notifying_tables. It also listens to the patient change signals sent from within its own client. Whenever the active patient changes the thread reconfigures itself to only listen for those patient-related backend signals which are specific to that patient -- to that end the backend signals are tagged with patient primary keys where appropriate (see above). The signal is stripped of the patient ID and reinjected into the client for further processing via gmDispatcher.py. That way the frontend code only needs to listen to a generic signal (such as allg_mod_db) and can still be sure to receive signals relevant to the currently active patient only.

Going along with the above example: the top panel (the two-line pane at the top of the client) listens for the signal allg_mod_db. Note that this signal can arrive asynchronously at any time but in the context of the listener thread ! Since wxPython is not written to reliably do GUI operations outside the GUI thread (often the main thread but in any case the thread that initialized wxPython) care needs to be taken when acting upon signals. If any GUI work needs to get done (such as updating fields onscreen) the recommended procedure is to use wx.CallAfter() to execute the GUI code in the context of the GUI thread like this:
gmDispatcher.connect(signal = 'allg_mod_db', receiver = _on_allg_mod_db_in_listener_thread)

def _on_allg_mod_db_in_listener_thread(*args, *kwargs):
   wx.CallAfter(self._on_allg_mod_db_in_gui_thread, *args, *kwargs)

def _on_allg_mod_db(*args, *kwargs):
   update_allergies_display()

This code makes allergies entered by any client automagically appear in all other clients displaying the same patient.

2) Conflicting patient record updates

GNUmed only ever "takes a write lock" on a row when the row is actually about to be written to. It does not:

  • lock it
  • let the user edit the data
  • then save it

but rather

  • read it
  • let the user edit the data
  • "lock" and save it

Assume this scenario as an example:

  • reception staff reconfirms allergy with patient
  • new information elicited for allergy entry
    • allergy to penicillin reported to have resulted in anaphylactic shock rather than simply a rash as had been recorded earlier
  • nurse starts editing the allergy entry but gets interrupted by phone call and scurries off for some lab work while the allergies editor stays open
  • patient moves to exam room
  • doctor reconfirms allergies before starting treatment
  • careful history taking reveals the patient "simply" had medium dyspnoe rather than a full-blown anaphylactic reaction
  • doctor records the newly found change to the allergies record
  • nurse finishes phone call and continues entering what she knows as the new allergy information

There are two potential problems in this situation:

1) Two overlapping transactions try to update the same row

This occurs very rarely and happens in the following sequence of events (note how it only concerns itself with two concurrent transactions):

transaction A transaction B
BEGIN  
  BEGIN
... UI work ... ... UI work ...
COMMIT  
  COMMIT

GNUmed uses the transaction level serializable which means transaction B will fail at the COMMIT step. This is standard transactional behaviour. Using the serializable transaction level is what amounts to the "write lock" alluded to above. GNUmed doesn't ever really take an actual lock since MVCC and transaction semantics afford the needed behaviour.

Note that since GNUmed keeps the duration of updating transactions to a minimum (by only starting them just-in-time) the probability of such serialization conflicts are very rare.

In the above scenario it would require that doctor and nurse start saving their respective allergy update virtually at the same time.

2) Two user actions framing each other try to update the same row

This is way more likely to occur than with overlapping transactions as described above. The sequence of events is as follows (note how this concerns itself with user actions reaching beyond the duration of a single transaction):

nurse doctor
reads row (begin; ...; commit;)  
  reads row (begin; ...; commit;)
changes row in UI  
  changes row in UI
  updates row (begin; ...; commit;)
updates row (begin; ...; commit;)  

There is no open transaction from when a read finishes until an update starts. Therefore the doctor's change to the row commits successfully. However, the nurse's change would commit successfully, too without her being aware of the intermittent change by the doctor ! The reason being that the database transactions do not actually conflict even in serializable mode.

One of the solutions to this concurrency problem is Optimistic Locking. GNUmed employs the change indicator column strategy ("mark the source with a unique identifier") to detect intermittent updates (google for "handy proxy for row has not changed is to see if its XMIN system column is still the same"). The update indicator comes cheap in the form of the XMIN column of PostgreSQL. XMIN is the ID of the database transaction which last modified the row. If a row changes XMIN changes, too. This is a core feature of PostgreSQL's MVCC architecture and is thus very unlikely to disappear (google for " XMIN semantic at peril").

There is a base class cBusinessDBObject in client/pycommon/gmBusinessDBObject.py which implements the optimistic locking for all it subclasses. Each instance represents a row in the database. When the table data is read from the backend the corresponding XMIN and primary key value are remembered. Later on when the data has been updated and is to be written back to the backend the class employs the following construct in the WHERE clause of its update query:
update <table> set
   <column> = <value>
   ...
where
   <pk column> = <pk value of this row> and
   XMIN = <original XMIN value>

Assuming the query is syntactically correct three situations can arise:

  • the original row has been deleted by another writer
    • neither XMIN nor the primary key will match any row so no row is updated which is detected as a concurrency conflict

  • the original row has been updated by another writer
    • the primary key will find the row in question but XMIN will have changed so it doesn't match any row and no row is updated which indicates that a concurrent change happened which, again, is detected as a concurrency conflict

  • the original row has not been touched
    • both the primary key and XMIN will find the row in question and allow the update to proceed so one row is updated and all is well

GNUmed needs to improve the handling of detected deletions and concurrent updates. Currently it at least detects them and refuses to silently overwrite existing medical data.
Topic revision: 20 Jan 2013, JamesBusser
 
Download.png
This site is powered by the TWiki collaboration platformCopyright © by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding Foswiki? Send feedback
Powered by Olark