Retail:Developers Guide/Concurrent Modification and Locking
Contents |
Introduction
Most of the times, the WebPOS application generates new data, usually via new documents (receipts) that are created and synchronised to the backend. While creating new data, the concurrent activity of several terminals is not a problem, because each one is creating new documents which are not really related between each other, and therefore their simultaneous activity doesn't generate any sort of conflict.
However, there are some cases in which the WebPOS can modify existing data. The purpose of this document is to explain how this situation is handled, and provide some guidelines and tips for other cases in which data modification may be necessary.
Risks of concurrent modification
When data is modified, there is always the risk that two terminals may be changing the same document or record at the same time. This creates the possibility of conflict between the changes. This situation can be handled differently depending on the case.
Currently in the WebPOS application, the main flow which modifies existing data is the Layaways flow. In this flow, a layaway is initially created, and then partial payments may be subsequently added, until a final payment triggers the completion of the order, and the generation of the shipment.
As these actions involve a modification of an existing record (in this case, the layaway which was saved already in the backend), they may suffer the problems of a concurrent modification if some users mistakenly open the same document at the same time in two different terminals and execute some actions on it.
To prevent problems in this case, some locking strategy must be followed. The strategy which should be followed depends heavily on the use case: the best strategy may be different if the probability of having concurrent modification is very low, or very high.
Optimistic and Pessimistic locking
Two main strategies are commonly discussed when trying to lock records in a system: optimistic and pessimistic locking:
- Optimistic locking assumes that the probability of having a concurrent modification in the same record is small, and therefore always allows the user to load any record, and only checks for potential conflicts when the record is then sent back to the server.
- Pessimistic locking assumes that it's very likely to have different users changing the same record concurrently, so when a user loads a record, this record is then locked, and other users cannot load it.
Optimistic is generally much easier to implement, and generates less overhead in most cases, unless the chance of having concurrent modifications is very high. Pessimistic locking requires taking care of removing the lock when its no longer applicable, and this doesn't work very well most of the times in a Web application, which the user can close at any time without much control from the developer, and besides, it's quite tricky to implement, leading to development mistakes and errors which impact the end user.
Currently in the WebPOS we use optimistic locking to handle the Layaways flow. The reason for that is that we consider that the chances of users modifying the same document concurrently are very low: most of the times, the user comes with the printed ticket that allows the cashier to identify which layaway needs to be loaded and modified. This printed ticket even comes with a barcode which can be scanned to prevent human error when introducing the document number, and even if the cashier inputs the document number manually, he will immediately notice if the document is the wrong one, as the information will not correspond with the printed document. Therefore, we consider optimistic locking the best strategy for handling this functionality.
We strongly believe that optimistic locking is the best approach in most cases. It's important to note that, if properly implemented, optimistic locking doesn't provide less safety or robustness than pessimistic locking when concurrent operations happen, it just provides a slightly different user experience. The only difference in terms of behaviour is that pessimistic locking would prevent the second user from opening the record, but optimistic locking will successfully detect and prevent the conflict with the same robustness, and technically, optimistic locking is much simpler to implement, and also causes less headache to users in many cases. An example of this would be that in case a user opens a record, using pessimistic locking the record would not be freed unless the user closes it or the lock itself expires, so if the user leaves the application open and leaves his computer without closing the record, other users won't be able to open the record in other terminals. This can be quite annoying, particularly in the case of Web applications with long valid sessions, which is quite frequently the case in point of sale terminals.
We currently use optimistic locking in the WebPOS both in the Layaways (or in general, orders) flow, and also in the Customer/Customer Address modification flows. Nowadays, there are no other flows in the WebPOS with the risk of concurrent modification
How to implement optimistic locking
The implementation of optimistic locking is quite simple, and works very well with the Openbravo audit information columns.
The main idea is to get the timestamp of the last modification in the record when it is initially loaded in the application. After that, this timestamp needs to be sent again when the changes are going to be synchronised to the backend. When synchronisation occurs, this timestamp is checked against the current timestamp of the record, and if they are no longer the same, then a concurrent modification has happened while the record was being changed in the application, and we can potentially have a conflict, so the change should be rejected.
This is the main principle: we get the timestamp, and then we use this timestamp when synchronising to verify that no changes were done in the meantime.
The way we implement this in Openbravo is using the updated column of the relevant record, which always contains the timestamp of the last modification. We load this information, and send it again, and compare it against the current value of the updated column. So, the full flow should do the following:
- When a record is loaded in the frontend, the updated value of the record should also be loaded.
- The user then does whatever actions he needs, and finally saves the record, so the record is synchronized to the backend
- In the process which updates the record, the previously loaded "updated" value should be checked against the current value of the "updated" column. If they are the same, then we can be sure that no concurrent modification happened, and the record can be safely saved. However, if both values differ, then there was a concurrent modification and the process should fail.
In practice, you will probably need to add the updated property to some ProcessHQLQuery class, for instance:
add(new HQLProperty("ord.updated", "loaded"));
And later on, in your synchronization process, you will need to do some initial check to ensure that the record was not changed in the meantime:
final Date loaded = POSUtils.dateFormatUTC.parse(jsonorder.getString("loaded")), updated = OBMOBCUtils .convertToUTC(order.getUpdated()); if (loaded.compareTo(updated) != 0) { throw new OutDatedDataChangeException(Utility.messageBD( new DalConnectionProvider(false), "OBPOS_outdatedLayaway", OBContext.getOBContext() .getLanguage().getLanguage())); } }
It is good practice to generate an OutdatedDataChangeException, so that we can clearly identify exceptions which happened due to a concurrent modification of the record. You should set a clear message in the exception which explains the problem to users in a meaningful way.
Back to Concepts