Data Editing
By data editing, we mean such actions as taking orders, handling order items, making table reservations, registering guests, and so on. Each individual action makes a small pinpoint change, for example, AddOrderItemProduct
adds an item to the order, whereas, AddOrderItemModifier adds a modifier to the item. Many individual actions can result in data inconsistency, for example, if an item has required modifiers, then if added without modifiers, this would break a corresponding subject matter rule. Users can add an item to the order with modifiers combining actions. In which case, it is important that actions would take place according to the «All or Nothing» rule and would convert the data from one consistent state to another consistent state. To ensure transactionality when editing the data, we introduce the concept of the editing session.
Editing Session
Editing session is a kind of database transactions. All actions, even individual ones, are carried out within sessions the following way:
- The
IEditSession
is started by callingPluginContext.Operations.CreateEditSession
. - One or more actions are carried out within the session.
- Made changes are saved using the
PluginContext.Operations.SubmitChanges
method.
Changes made in step two cannot be seen until saved. In step three, all changes are whether stored successfully or rolled back, and an exception is generated.
Syncing
Access to data is synced using interlocks and revisions. Objects are locked during the editing and it is impossible to control such locks directly via the API as objects are locked automatically when the changes are saved (SubmitChanges
). This matches the concept of optimistic locks: when in steps of creating the editing session and performing actions, objects are not locked, but when the changes are being saved, a check takes place verifying whether or not anything is changed by any other user. When saved, objects have a new revision assigned, which makes it possible to tell different versions of the object from one another. With account of low demand for the concurrent editing of the same objects, this approach makes the UI simpler (no locking management methods are required, no need to think of their proper release) and ensures the data availability (an object cannot be locked for long, locks cannot be left unreleased, in case of the emergency plugin shutdown, the object will not freeze in locked condition).
Certain programming specifics should be kept in mind:
- The very Syrve POS app can lock objects for a long time (pessimistic locks). For example, when a user goes to the order editing screen, a corresponding order will be locked at least until the screen is open (then it depends on the screen a user goes to next). During this time, the locked order cannot be edited via the API. Waiters should probably be advised to lock the screen when they leave the POS or set a short auto-lock timeout (10 min by default). Besides, objects can be locked for a while regardless of any user activity (for example, some changes can be scheduled).
- When an object is locked, other related objects will be locked as well. Thus, when locking a banquet order, a corresponding reservation will be locked as well. If at least one object cannot be locked, the operation fails.
- To sync the data access properly, use Syrve Office to set the main cash register in the group settings. A plugin that uses data editing functions should preferably be installed on the main cash register. Other terminals can also be used but in certain scenarios, changes are declined, which is caused by the implementation details. Such limitations would probably be lifted in one of the later versions.
Continuous Execution
IOperationService
operations that require syncing for the data editing. They lock and then unlock objects at each call. Therefore, when several operations are invoked successively, each operation will lock data independently, make changes, and then unlock it. Other parties may wedge in between the operation calls with the intent to edit the same data; as a result, some operations are performed successfully and some return errors EntityAlreadyInUseException
, EntityModifiedException
, other operations may become inapplicable with reference to other changes.
For example, a delivery order with a posted external prepayment needs to be created in the plugin that accepts external delivery orders (website, aggregator). All this cannot be performed atomically within one editing session as the payment submission is irreversible and is performed separately, therefore, you need to create a delivery order first and fill up its fields, adding unposted external prepayment (CreateEditSession
, CreateDeliveryOrder
, AddExternalPaymentItem
, etc.), save the changes (SubmitChanges
), and then try to submit the prepayment (ProcessPrepay
). The data remains unlocked between these operations (SubmitChanges
and ProcessPrepay
) and is available for editing to anybody. Those who subscribed to delivery changes can be notified of the new delivery order just in time to make changes before the prepayment is posted. Writing a code that can be interrupted and then keep running smoothly adapting to external changes is a quite time-consuming process. To make such scenarios work smoothly, we added the ability to perform several operations uninterruptedly or continuously.
ExecuteContinuousOperation
— a special operation that can be used to successively and continuously execute several other operations. Operations should be grouped into one function or lambda in the plugin code and sent as the callback to the ExecuteContinuousOperation
method that would invoke this callback and send a special instance of the IOperationService
service to the callback input, which intended for the continuous operation execution:
PluginContext.Operations.ExecuteContinuousOperation(
operations =>
{
...
operations.SubmitChanges(...);
...
operations.ProcessPrepay(...);
...
});
It should be noted that the ExecuteContinuousOperation
root operation is invoked via the PluginContext.Operations
general service, whereas included operations — via the service instance received by lambda at the input (which is called operations
in the example above). Technically speaking, operations invoked through a special service instance work the same way but do not unlock data once they are executed, which means each operation that requires syncing locks the data if it hasn’t been locked previously by other operations, makes changes, and leaves the data locked for the next operations. This ensures that no other entity can «steal» the lock and wedge in, and our successive operations with the same objects would not get the EntityAlreadyInUseException
. The data will be unlocked when the control is returned from lambda.
The following limitations should be considered:
- This function has nothing to do with atomicity or transactionality, each embedded operation is carried out individually and saves changes immediately. If any of the operations fail (exception generated), preceding operations will not be canceled.
- The operation continuity does not mean that no other party can do anything. It means that consistent editing of a particular object cannot be interrupted. Other plugins and the Syrve POS app can edit some other objects at the same time. Such objects should not be locked or changed by us. The data is locked as and when necessary, therefore, if we decide to change an object while executing the continuous sequence after successful performance of operations on another object, we would probably get the
EntityAlreadyInUseException
. - Since affected data remains locked during the entire lambda operation time sent to
ExecuteContinuousOperation
, which can inhibit user operations, other plugins, and app functions, the sequence of operations should be executed fast. Within the continuously executed session, requests to external services and equipment should be performed with caution without lingering over them or better yet avoiding external I/O. Whenever possible, perform the preparation in advance outside the continuously executed session. If it is not possible, make sure there are reasonable idle timeouts.
Stubs
Since no action results are available before saving the entire session, it is required to refer to a new but not yet existing object while executing an operational sequence within one session. For example, users may want to add guests to the order, then add items to a guest, and modifiers to an item even though there is no order yet, no guests, no items. For this reason, we introduce a concept of stubs — certain fake, yet unambiguous, object references. Object creation actions, such as CreateOrder
or AddOrderGuest
return INew...Stub
stubs that can be used instead of other objects within the same session.
Most editing methods take such stubs as arguments, which makes it possible to send both existing and new objects to these methods. For example, the SetOrderType
method takes IOrderStub
, therefore, a type can be set to both an existing order (IOrder
:
IOrderStub
) and a new order (INewOrderStub
:
IOrderStub
).
However, some actions can require strictly one of the two — new or existing object, in which case, one of the heirs (rather than the base type) will be used in the method signature.
Expected Exceptions
When saving changes various exceptions may occur. Some of them may indicate the plugin code error (for example, ArgumentNullException
or ArgumentOutOfRangeException
); we do not recommend suppressing such exceptions (it is preferable to fix the code error). However, some exceptions can be neither predicted nor prevented — they should rather be intercepted and processed properly:
EntityAlreadyInUseException
— attempt to apply changes to a locked object. It can be retried later.EntityModifiedException
— attempt to apply changes to an old object version. It means that after the plugin read the object, this object was changed by any other party. The object needs to be reread; and if scheduled changes are still required, they should be reapplied.PermissionDeniedException
— attempt to perform any action without sufficient permissions. If a user wants the plugin to perform such actions, it needs to be provided with respective permissions using Syrve Office.- …
Syntactic Sugar
It so sometimes happens that only one action is required, in which case, creating the editing session seems cumbersome. For this reason, the IOperationService has auxiliary extension methods programmed that create the editing session, perform the action required, save changes, and return the action result. Technically, the same could be written manually. We do not recommend using such wrappings if more than one action should be concurrently performed.