Template Examples - Locker"

From Documentation
m (correct highlight (via JWB))
 
(89 intermediate revisions by 2 users not shown)
Line 1: Line 1:
 
{{Template:Smalltalk_Author|
 
{{Template:Smalltalk_Author|
 
|author=Robert Wenzel, Engineer, Potix Corporation
 
|author=Robert Wenzel, Engineer, Potix Corporation
|date=January XX, 2018
+
|date=January 11, 2018
|version=ZK 8.5
+
|version=ZK 8.5.0
 
}}
 
}}
 +
 +
Special thanks go to Dirk Deyne (Belgium) for providing idea and motivation inspiring this article.
  
 
= Introduction =
 
= Introduction =
  
 +
When multiple users operate on the same data it is likely that 2 or more users are trying to edit the same object simultaneously.
 +
At DB level you can address this with strategies like optimistic or pessimistic locking (each having specific performance/consistency characteristics).
 +
 +
Propagating DB locking concerns down to the UI and eventually the User (Browser) can be quite risky and unstable, as we never know how fast/reliable a user/browser/network connection will respond - resulting in resources being locked even after a user has already disconnected.
 +
 +
Doing too little will result in surprised/frustrated users losing their work, because another user was faster.
 +
 +
In the following article I'll introduce a simple UI based mechanism to avoid editing conflicts without expensive DB locks.
  
 
= What we want =
 
= What we want =
  
Our goal is to avoid multiple users from editing the same resource simultaneously we need the following functionalities.
+
To achieve goal to build a UI that prevents multiple users from editing the same resource simultaneously, we need the following functionalities:
  
# '''lock / unlock''' a resource
+
# '''lock / unlock''' a resource (request a lock)
#:e.g. obtain and release exclusive access to edit a specific objects properties
+
#:e.g. obtain and release exclusive access to edit a specific object and its properties
 
# '''observe''' a resource for owner changes (LockEvents)
 
# '''observe''' a resource for owner changes (LockEvents)
 
#:it should be possible to observe the same resource with multiple concurrent users
 
#:it should be possible to observe the same resource with multiple concurrent users
Line 26: Line 36:
  
 
Before going into details here a small example illustrating what we are trying to achieve.
 
Before going into details here a small example illustrating what we are trying to achieve.
<gflash width="910" height="657">template-examples_simpleLockable.swf</gflash>
+
{{#ev:youtubehd|Agzz0IgYUUM}}
  
Here 2 users in 2 separate browsers are seeing (observing) the same resource - initially not locked by anyone.
+
Here 3 users in 3 separate browsers (Chrome/FF/Edge) are seeing (observing) the same resource - initially not locked by anyone.
As soon as User-1 locks the resource it becomes editable for him. At the same the lock button disappears for the User-2 - replaced by a message showing the lock status and owner. When unlocking the resource it becomes available for both users again. Then User-2 repeats the cycle. As simple as that.
+
As soon as User-1 locks the resource it becomes editable only for him. At the same time the "lock" buttons disappear for the User-2 and User-3 - replaced by a message showing the lock status and owner. When unlocking the resource it becomes available for all users again. Then User-2 repeats the cycle, and finally User-3.
  
 
= How does it work =
 
= How does it work =
  
The example above is implemented in simpleLockable.zul and SimpleLockableViewModel.java shouldn't contain too many surprises - Which is the intention of this article -> to show a simple way to achieve that.
+
The example above implemented in simpleLockable.zul and SimpleLockableViewModel.java shouldn't contain too many surprises - Which is the intention of this article -> to show a simple way to achieve that.
  
 
== simpleLockable.zul ==
 
== simpleLockable.zul ==
 
+
[https://github.com/zkoss-demo/zk-template-examples/blob/locker/src/main/webapp/locker/simpleLockable.zul simpleLockable.zul]
<source lang="xml" high="14,16,19,25,27,33,39">
+
<source lang="xml" highlight="14,16,19,25,27,33,39">
 
<zk>
 
<zk>
 
<style>
 
<style>
Line 86: Line 96:
 
</source>
 
</source>
  
*'''Line 14:''' dynamic property ''sclass'' changing by lock status
+
*'''Line 14:''' dynamically [http://books.zkoss.org/zk-mvvm-book/8.0/syntax/load.html @load] the ''sclass'-property changing with the lock status
*'''Lines 16,19:''' render the resource editable for status ''OWNED'' (using the shadow elements <choose>/<when>/<othewise> '''LINK ME''')
+
*'''Lines 16,19:''' render the resource editable for status ''OWNED'' (using the shadow elements [http://books.zkoss.org/zk-mvvm-book/8.0/syntax/choose.html <choose>/<when>/<otherwise>])
*'''Lines 25,27,33,39:''' apply ('''LINK ME''') dynamic templates to add controls/information for all 3 different lock states
+
*'''Lines 25,27,33,39:''' [http://books.zkoss.org/zk-mvvm-book/8.0/syntax/apply.html <apply>] dynamic templates to update controls/information for all 3 different lock states
  
In the zul file above all the render decisions are based on the EL <code>vm.lockable.status</code> for now it's sufficient to know that this ''status'' property will "somehow" contain and update the current lock status (synchronized for all users).  
+
In the zul file above all the render decisions are based on the EL <code>vm.lockable.status</code> for now it's sufficient to know that this ''status'' property will "somehow" contain and update the current lock status (synchronized for all users).
  
 
== SimpleLockableViewModel ==
 
== SimpleLockableViewModel ==
Line 96: Line 106:
 
Also the view model doesn't contain a lot of logic:
 
Also the view model doesn't contain a lot of logic:
  
<source lang="java" high="5,10,11" >
+
[https://github.com/zkoss-demo/zk-template-examples/blob/locker/src/main/java/zk/example/template/locker/SimpleLockableViewModel.java SimpleLockableViewModel.java]
 +
<source lang="java" highlight="5,10,11,26" >
 
public class SimpleLockableViewModel {
 
public class SimpleLockableViewModel {
 
private static final AtomicInteger userCounter = new AtomicInteger(0);
 
private static final AtomicInteger userCounter = new AtomicInteger(0);
Line 122: Line 133:
 
public void unlock() { lockTracker.unlock(); }
 
public void unlock() { lockTracker.unlock(); }
  
public UiLockable<SimpleResource> getLockable() { return lockTracker.getLockable(); }
+
public Lockable<SimpleResource> getLockable() { return lockTracker.getLockable(); }
  
 
public String getUsername() { return username; }
 
public String getUsername() { return username; }
Line 128: Line 139:
 
</source>
 
</source>
  
Worth mentioning is '''line 10''' that we enable server-push ('''LINK ME''') as a prerequisite to allow for asynchronous UI updates.
+
Worth mentioning is '''line 10''' - enabling [[ZK_Developer%27s_Reference/Server_Push/Synchronous_Tasks | server-push]] as a prerequisite to enable UI updates from background threads.
  
Besides the usual @Command-bindings and getters, the only things remaining are the <code>UiLockTracker</code> ('''line 5''') and <code>UiLockable/MvvmLockable</code> ('''line 11''') classes.  
+
Besides the usual @Command-bindings and getters, the only things remaining are the [[#UiLockTracker | <code>UiLockTracker</code>]] ('''line 5''') and [[#Lockable/MvvmLockable | <code>Lockable/MvvmLockable</code>]] ('''line 11,26''') classes, explained later. [https://github.com/zkoss-demo/zk-template-examples/blob/locker/src/main/java/zk/example/template/locker/domain/SimpleResource.java <code>SimpleResource</code>] is a trivial Java-bean with a single property: ''value'' - Since Lockable/UiLocktracker are generic this can be any arbitrary type.
  
 +
== Lockable/MvvmLockable ==
  
== UiLockable/MvvmLockable ==
+
In order to "calculate" the lock status the Lockable needs a ''resource'', the current user (''self'') and the lock ''owner'' information.
 
 
In order to "calculate" the lock status the UiLockable needs a ''resource'', the current user (''self'') and the lock ''owner'' information.
 
 
Owner changes are indicated with a LockEvent whenever onLockEvent() is called.
 
Owner changes are indicated with a LockEvent whenever onLockEvent() is called.
  
<source lang="java">
+
[https://github.com/zkoss-demo/zk-template-examples/blob/locker/src/main/java/zk/example/template/locker/lockable/Lockable.java Lockable.java]
public class UiLockable<T> {
+
<source lang="java" highlight="21,22,23,24">
 +
public class Lockable<T> {
 
private final String self;
 
private final String self;
 
private final T resource;
 
private final T resource;
 
private String owner;
 
private String owner;
  
public UiLockable(String self, T resource) {
+
public Lockable(String self, T resource) {
 
this.self = self;
 
this.self = self;
 
this.resource = resource;
 
this.resource = resource;
Line 174: Line 185:
 
When using MVVM the changing properties can be notified automatically as in the implementation below.
 
When using MVVM the changing properties can be notified automatically as in the implementation below.
  
 +
 +
[https://github.com/zkoss-demo/zk-template-examples/blob/locker/src/main/java/zk/example/template/locker/lockable/MvvmLockable.java MvvmLockable.java]
 
<source lang="java">
 
<source lang="java">
public class MvvmLockable<T> extends UiLockable<T> {
+
public class MvvmLockable<T> extends Lockable<T> {
 
...
 
...
 
@Override
 
@Override
Line 186: Line 199:
 
</source>
 
</source>
  
Additional notifications of vm properties are implemented by overriding the onLockEvent again.
+
Additional notifications of vm properties can be implemented by overriding the onLockEvent as needed.
  
<source>
+
<source lang="java">
lockTracker.observe(new MvvmLockable<SimpleResource>(getUsername(), sharedResource) {
+
new MvvmLockable<SimpleResource>(getUsername(), sharedResource) {
 
@Override
 
@Override
 
public void onLockEvent(LockEvent event) {
 
public void onLockEvent(LockEvent event) {
Line 195: Line 208:
 
BindUtils.postNotifyChange(null, null, sharedResource, "value");
 
BindUtils.postNotifyChange(null, null, sharedResource, "value");
 
}
 
}
});
+
}
 
</source>
 
</source>
  
Notifying the '''sharedResource''' property will make sure the rendered resource value is also updated in all observing browsers after a lock event. This especially allows conditional notification for different requirements in your UI.
+
Notifying the '''sharedResource.value''' property will make sure the rendered resource value is also updated in all observing browsers after a lock event. This especially allows conditional notification for different requirements in your UI.
  
 
So far we haven't seen how those LockEvents are triggered and distributed among the observing clients. Let's get into the details.
 
So far we haven't seen how those LockEvents are triggered and distributed among the observing clients. Let's get into the details.
Line 204: Line 217:
 
== UiLockTracker ==
 
== UiLockTracker ==
  
The '''UiLockTracker''' is surely the core class of this example.
+
The [https://github.com/zkoss-demo/zk-template-examples/blob/locker/src/main/java/zk/example/template/locker/lockable/UiLockTracker.java <code>UiLockTracker</code>] is surely the core class of this example - providing a convenient way to observe LockEvents on a Lockable, which we have already seen in the code above following the simple life cycle:
 +
 
 +
#observe - start observing a resource by providing a Lockable implementation to receive LockEvents
 +
#lock - request a lock on a Lockable
 +
#unlock - release the lock
 +
#reset - stop observing the lockable
 +
 
 +
Between 1 and 4 it is possible receive and handle LockEvents for that resource.
 +
 
 +
=== Usage ===
 +
 
 +
<source lang="java" highlight="1,4,6">
 +
private final UiLockTracker<SimpleResource> lockTracker = new UiLockTracker<>(10, 5);
 +
...
 +
 
 +
lockTracker.observe(new MvvmLockable<SimpleResource>(getUsername(), sharedResource) {
 +
@Override
 +
public void onLockEvent(LockEvent event) {
 +
super.onLockEvent(event);
 +
BindUtils.postNotifyChange(null, null, sharedResource, "value");
 +
}
 +
}
 +
</source>
 +
 
 +
*'''line 1:''' Create a '''UiLockTracker''' instance (the parameters are explained in the next paragraph)
 +
*'''line 4:''' Observe the '''sharedResource''' with the current user
 +
*'''line 6:''' Override '''onLockEvent''' to handle LockEvents
 +
 
 +
It provides the methods '''lockTracker.lock'''/'''unlock''' to request/release a lock - when succeeding those will emit the related <code>LockEvent</code>s asynchronously resulting in calls to onLockEvent for each observing user.
 +
 
 +
Since we already use the MvvmLockable class the UI will update automatically by calling @load-bindings on component properties or shadow elements to re-apply templates.
 +
 
 +
Once the resource doesn't need to be observed any more a call to '''lockTracker.reset()''' performs the cleanup (not used in this simple example - it only uses a single object).
 +
 
 +
=== Implementation ===
 +
'''Disclaimer:''' This implementation doesn't intend to be fully complete or stable - it also won't work in a clustered environment.
 +
 
 +
The goal of this article is not to explain how to perform 100% water tight resource locking, but to build a UI for the user to observe a resource and its status avoiding user error (not implementation error).
 +
So I'll only cover the ZK specific parts. If you are interested in RxJava2 please refer to the official documentation.
 +
 
 +
Internally a [http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/subjects/BehaviorSubject.html BehaviorSubject] is used for each observed Lockable - which ensures each subscriber receives the last cached and subsequent LockEvents.
 +
 
 +
<source lang="java" highlight="2,5,6">
 +
private void subscribeToLockEvents(Lockable<T> lockable) {
 +
lockEventSubscription = LockService.observeResource(lockable.getResourceKey())
 +
.doOnNext(event -> System.out.println("locked: " + event.owner + " on " + event.getResourceKey()))
 +
.doOnNext(event -> toggleAliveCheck(event, lockable))
 +
.compose(ZkObservable.activated())
 +
.subscribe(lockable::onLockEvent, this::resetOnError);
 +
}
 +
</source>
 +
*'''line 2:''' create/join the resource subject for the resource key
 +
*'''line 5,6:''' call onLockEvent inside an activated execution to allow UI updates
 +
 
 +
The composable ZkObservable.activated() ObservableTransformer is already explained in my [https://dzone.com/articles/deal-with-hot-observables-in-a-web-ui previous article on DZone] - it activates/deactivates the current desktop for modification allowing asynchronous UI updates via server push.
 +
 
 +
The sequence - observe -> lock -> unlock -> reset - is a straight forward implementation as long as things are under control.
 +
 
 +
There are certain occasions when things go '''wrong''':
 +
* user forgets to release the lock (while the session times out)
 +
* user closes/refreshes the browser tab
 +
* the network disconnects
 +
* the browser process crashes or terminates
 +
 
 +
For such cases UiLockTracker implements 2 automatic cleanup mechanisms to ensure a resource becomes available again and observers are removed from the RX Subjects to avoid memory leaks.
 +
 
 +
==== DesktopCleanup ====
 +
1. A [https://www.zkoss.org/wiki/ZK_Configuration_Reference/zk.xml/The_listener_Element/The_org.zkoss.zk.ui.util.DesktopCleanup_interface DesktopCleanup listener] is dealing with the common scenario - where a user closes/refreshes the browser tab and the case where the session times out due to inactivity.
 +
 
 +
<source lang="java" highlight="3">
 +
private void addDesktopCleanup() {
 +
removeDesktopCleanup();
 +
desktopCleanup = dt -> this.reset();
 +
desktop.addListener(desktopCleanup);
 +
}
 +
 
 +
private void removeDesktopCleanup() {
 +
if(desktopCleanup != null) {
 +
desktop.removeListener(desktopCleanup);
 +
this.desktopCleanup = null;
 +
}
 +
}
 +
</source>
 +
 
 +
'''line 3''': calls reset() automatically when the desktop is destroyed.
 +
 
 +
This is enough for users NOT owning a lock since it doesn't really impact other users even if they keep observing until their session times out.
 +
However for users holding a lock a second mechanism is in place.
 +
 
 +
==== Alive-check ====
 +
2. An '''alive-check''' to actively check whether the connection to a user owning a lock is still responding during the time they own the lock.
 +
 
 +
The implementation uses a second Observable to poll the client in configurable intervals by activating/deactivating the desktop for an instant of time.
 +
 
 +
The interval (10sec) and timeout (5sec) are configured when creating the lockTracker instance.
 +
<source lang="java">
 +
private final UiLockTracker<SimpleResource> lockTracker = new UiLockTracker<>(10, 5);
 +
</source>
 +
 
 +
The implementation tries to activate the desktop after each interval. This triggers a client-side round trip without any UI changes - just making sure the client is still responding.
 +
 
 +
<source lang="java" highlight="3,5,12">
 +
private void startAliveCheck() {
 +
System.out.println("alive check: started, " + lockable);
 +
aliveCheckSubscription = Observable.interval(aliveCheckInterval, TimeUnit.SECONDS)
 +
.compose(ZkObservable.activated())
 +
.timeout(aliveCheckTimeout + aliveCheckInterval, TimeUnit.SECONDS)
 +
.subscribe(
 +
aliveCount -> {
 +
System.out.println("alive check: " + (aliveCount + 1) * aliveCheckInterval + "sec, " + lockable);
 +
},
 +
err -> {
 +
System.err.println("Unlocking unresponsive lock owner: " + lockable + " - " + err);
 +
unlock();
 +
});
 +
}
 +
 
 +
private void stopAliveCheck() {
 +
if(aliveCheckSubscription != null) {
 +
System.out.println("alive check: stopped, " + lockable);
 +
aliveCheckSubscription.dispose();
 +
aliveCheckSubscription = null;
 +
}
 +
}
 +
</source>
 +
*'''line 3:''' start a periodic Observable to perform alive check with [http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/Observable.html#interval-long-java.util.concurrent.TimeUnit- interval()]
 +
*'''line 5:''' [http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/Observable.html#timeout-long-java.util.concurrent.TimeUnit- timeout-operator] triggers an exception when activation takes too long
 +
 
 +
 
 +
 
 +
When an error occurs (e.g. when the timeout is exceeded or desktop activation fails) <code>unlock()</code> is called to make the resource available again.
 +
 
 +
== LockService ==
 +
 
 +
This simplified dummy back-end implementation is based on RxJava. It keeps a map of BehaviorSubjects (one for each resourceKey).
 +
Each subject will emit [https://github.com/zkoss-demo/zk-template-examples/blob/locker/src/main/java/zk/example/template/locker/lockservice/LockEvent.java <code>LockEvent</code>s] to inform all subscribers of the current lock owner. Subscribers decide how to handle the events accordingly.
 +
 
 +
The [https://github.com/zkoss-demo/zk-template-examples/blob/locker/src/main/java/zk/example/template/locker/lockservice/LockService.java <code>LockService</code>] will also check/close idle Subjects in fixed intervals (currently 20 seconds - hard coded)
 +
 +
This implementation doesn't perform any sanity checks, simply the last call to lock/unlock will determine the next event. It is not the goal of this example to implement transaction safety.
 +
 
 +
Key is: The UI has to reflect every decision the back-end makes. Whatever the reason (e.g. due to higher privileges), the UI has to react in some way (reflecting the latest state), following the [https://en.wikipedia.org/wiki/Single_source_of_truth single source of truth] principle - to give the user an up-to-date view of the application state.
 +
 
 +
For this example I decided it's better to inform the user as early as possible than letting him edit a resource and telling him at the end, that he can't save due to a conflict.
 +
 
 +
Another UI implementation might decide to keep an item editable (even after losing the lock) - it will then have to deal with possible save/merge conflicts later. Your back-end will hopefully inform you about (and help you with) potential merge conflicts (Optimistic vs Pessimistic locking).
 +
 
 +
= A more complex UI example =
 +
 
 +
Using the same UiLockTracker and Lockable objects you can build a more complex UI such as the following inventory example.
 +
 
 +
It shows how varying resources can be observed/locked on demand and gives some ideas how to give obvious feedback to the observer.
 +
Both visualizing the LockState and enabling only the allowed interactions. If one InventoryItem is already locked/edited the UI remains responsive allowing to view/edit a different InventoryItem.
 +
 
 +
== InventoryViewModel ==
 +
[https://github.com/zkoss-demo/zk-template-examples/blob/locker/src/main/java/zk/example/template/locker/InventoryViewModel.java InventoryViewModel.java]
 +
 
 +
<source lang="java" highlight="16,17">
 +
public class InventoryViewModel {
 +
private static final AtomicInteger userCounter = new AtomicInteger(0);
 +
private final String username = "user-" + userCounter.incrementAndGet();
 +
 
 +
private ListModelList<InventoryItem> inventory;
 +
private final UiLockTracker<InventoryItem> lockTracker = new UiLockTracker<>(10, 5);
 +
 
 +
@Init
 +
public void init(@ContextParam(ContextType.DESKTOP) Desktop desktop) {
 +
desktop.enableServerPush(true);
 +
inventory = new ListModelList<>(InventoryService.getInventory());
 +
}
 +
 
 +
@Command
 +
public void view(@BindingParam("item") InventoryItem item) {
 +
lockTracker.reset();
 +
lockTracker.observe(new MvvmLockable(username, item));
 +
BindUtils.postNotifyChange(null, null, this, "currentItem");
 +
}
 +
 
 +
@Command
 +
public void edit(@BindingParam("item") InventoryItem item) {
 +
view(item);
 +
lockTracker.lock();
 +
}
 +
 
 +
@Command
 +
public void save(@BindingParam("item") InventoryItem item) {
 +
lockTracker.unlock();
 +
inventory.notifyChange(item);
 +
}
 +
 
 +
@Command
 +
public void cancel(@BindingParam("item") InventoryItem item) {
 +
lockTracker.unlock();
 +
}
 +
 
 +
public ListModelList<InventoryItem> getInventory() { return inventory; }
 +
 
 +
public Lockable<InventoryItem> getCurrentItem() { return lockTracker.getLockable(); }
 +
 
 +
public String getUsername() { return username; }
 +
}
 +
</source>
 +
 
 +
*'''line 16,17:''' observes a Lockable<InventoryItem> only when viewed (resets previous items before)
 +
 
 +
Other than that this is still a trivial view model with @Command handler methods and getters.
 +
 
 +
== inventory.zul ==
 +
[https://github.com/zkoss-demo/zk-template-examples/blob/locker/src/main/webapp/locker/inventory.zul inventory.zul]
 +
<source lang="xml" highlight="3,21,24,25,26">
 +
<?import zk.example.template.locker.lockable.LockStatus?>
 +
<?style src="/locker/template/lockableEditor.css"?>
 +
<?component name="lockableEditor" templateURI="/locker/template/lockableEditor.zul"?>
 +
<zk>
 +
<div viewModel="@id('vm') @init('zk.example.template.locker.InventoryViewModel')">
 +
You are user: ${vm.username}
 +
<grid model="@init(vm.inventory)">
 +
<template name="model">
 +
<row>
 +
<label value="@init(each.id)"/>
 +
<label value="@init(each.name)"/>
 +
<label value="@init(each.value)"/>
 +
<label value="@init(each.quantity)"/>
 +
<button label="view" iconSclass="z-icon-eye" onClick="@command('view', item=each)"/>
 +
<button label="edit" iconSclass="z-icon-edit" onClick="@command('edit', item=each)"/>
 +
</row>
 +
</template>
 +
</grid>
 +
 
 +
<if test="@load(!empty vm.currentItem)">
 +
<lockableEditor lockable="@load(vm.currentItem)" editCommand="edit" saveCommand="save" cancelCommand="cancel">
 +
<template name="content">
 +
<apply template="@load(lockable.status)" item="@init(lockable.resource)">
 +
<template name="AVAILABLE" src="/locker/template/inventoryItemView.zul"/>
 +
<template name="UNAVAILABLE" src="/locker/template/inventoryItemView.zul"/>
 +
<template name="OWNED" src="/locker/template/inventoryItemEdit.zul"/>
 +
</apply>
 +
</template>
 +
</lockableEditor>
 +
</if>
 +
</div>
 +
</zk>
 +
</source>
 +
 
 +
'''line 3,21:''' declare/use a custom <code><lockableEditor></code> element referencing an external template
 +
 
 +
'''line 24-26:''' render item based on lock status using inventoryItem'''View'''.zul or inventoryItem'''Edit'''.zul
 +
 
 +
The lockableEditor template can be defined/styled once and reused for different viewable/editable objects in an application (Persons, Addresses, etc.)
 +
 
 +
== lockableEditor.zul ==
 +
 
 +
This template wraps lockable resources by surrounding the editor with contextual information/controls based on the current LockStatus.
 +
 
 +
[https://github.com/zkoss-demo/zk-template-examples/blob/locker/src/main/webapp/locker/template/lockableEditor.zul lockableEditor.zul]
 +
<source lang="xml" highlight="20">
 +
<zk>
 +
<groupbox>
 +
<caption>
 +
<label value="@load((lockable.resource += ' - ' += lockable.status))"/>
 +
<choose>
 +
<when test="@load(lockable.status eq LockStatus.UNAVAILABLE)">
 +
<span sclass="z-icon-lock" style="color: red"/>
 +
locked by:
 +
<label value="@load(lockable.owner)"/>
 +
</when>
 +
<when test="@load(lockable.status eq LockStatus.OWNED)">
 +
<span sclass="z-icon-lock" style="color: green"/>
 +
locked by you
 +
</when>
 +
<otherwise/>
 +
</choose>
 +
</caption>
 +
 
 +
<div sclass="editorContent">
 +
<apply template="content"/>
 +
<apply template="@load(lockable.status)">
 +
<template name="AVAILABLE">
 +
<div sclass="availableOverlay" onClick="@command(editCommand, item=lockable.resource)"/>
 +
</template>
 +
<template name="OWNED">
 +
<div>
 +
<separator/>
 +
<button label="save" onClick="@command(saveCommand, item=lockable.resource)"/>
 +
<button label="cancel" onClick="@command(cancelCommand, item=lockable.resource)"/>
 +
</div>
 +
</template>
 +
<template name="UNAVAILABLE">
 +
<div sclass="unavailableOverlay"/>
 +
</template>
 +
</apply>
 +
</div>
 +
</groupbox>
 +
</zk>
 +
</source>
 +
*'''line 20:''' here the actual content is injected
 +
 
 +
After adding some CSS the result could look like this http://localhost:8080/locker/inventory.zul
 +
 
 +
[[File:template-examples-locker-inventory.png]]
  
 
= Summary =
 
= Summary =
 +
 +
Resource locking is an interesting but complex topic. Since the chances of parallel access are often small a minimal approach as explained above might already cover the majority of cases - without impact on the data source.
 +
 +
I invite you to play with the example, develop your own thoughts on the techniques used.
 +
I hope the example leaves room for your own creative idea on how to communicate the current lock status to the user (be it dynamic CSS or dynamic components/templates).
 +
 +
BTW: These techniques are not limited to locking only, you could observe any other information (prices, availability, booking reservations ...) and respond with UI changes accordingly. When using MVVM the principle remains the same: data changes trigger UI updates.
 +
 +
If you like the article or have ideas to improve this approach let me know in the comments below.
  
 
== Example Sources ==
 
== Example Sources ==

Latest revision as of 04:38, 20 January 2022

DocumentationSmall Talks2018JanuaryTemplate Examples - Locker
Template Examples - Locker

Author
Robert Wenzel, Engineer, Potix Corporation
Date
January 11, 2018
Version
ZK 8.5.0

Special thanks go to Dirk Deyne (Belgium) for providing idea and motivation inspiring this article.

Introduction

When multiple users operate on the same data it is likely that 2 or more users are trying to edit the same object simultaneously. At DB level you can address this with strategies like optimistic or pessimistic locking (each having specific performance/consistency characteristics).

Propagating DB locking concerns down to the UI and eventually the User (Browser) can be quite risky and unstable, as we never know how fast/reliable a user/browser/network connection will respond - resulting in resources being locked even after a user has already disconnected.

Doing too little will result in surprised/frustrated users losing their work, because another user was faster.

In the following article I'll introduce a simple UI based mechanism to avoid editing conflicts without expensive DB locks.

What we want

To achieve goal to build a UI that prevents multiple users from editing the same resource simultaneously, we need the following functionalities:

  1. lock / unlock a resource (request a lock)
    e.g. obtain and release exclusive access to edit a specific object and its properties
  2. observe a resource for owner changes (LockEvents)
    it should be possible to observe the same resource with multiple concurrent users
  3. react to LockEvents and update the UI
    immediately indicate availability/ownership/unavailability of a resource to each observing user

First we need a way to indicate our intention that we want to edit a resource and obtain a lock and vice versa return the lock (unlock) when we are done editing. This is only useful if other users are aware of that condition so they don't attempt concurrent editing (and potentially losing data or producing inconsistent merged results) - means we need to observe the current lock status and finally react in the UI. e.g. by changing labels, styles, disabling/hiding/removing/adding appropriate controls.

Simple example

Before going into details here a small example illustrating what we are trying to achieve.

EmbedVideo does not recognize the video service "youtubehd".

Here 3 users in 3 separate browsers (Chrome/FF/Edge) are seeing (observing) the same resource - initially not locked by anyone. As soon as User-1 locks the resource it becomes editable only for him. At the same time the "lock" buttons disappear for the User-2 and User-3 - replaced by a message showing the lock status and owner. When unlocking the resource it becomes available for all users again. Then User-2 repeats the cycle, and finally User-3.

How does it work

The example above implemented in simpleLockable.zul and SimpleLockableViewModel.java shouldn't contain too many surprises - Which is the intention of this article -> to show a simple way to achieve that.

simpleLockable.zul

simpleLockable.zul

<zk>
	<style>
		.lockIndicator { padding: 12px; display: inline-block; border-radius: 15px; }
		.lockIndicator .z-label { color: white; font-weight: bold; }
		.lockIndicator.OWNED { background-color: LimeGreen; }
		.lockIndicator.AVAILABLE { background-color: LightSteelBlue; }
		.lockIndicator.UNAVAILABLE { background-color: Crimson; }
	</style>
	<div viewModel="@id('vm') @init('zk.example.template.locker.SimpleLockableViewModel')">
		I am '${vm.username}'.
		<separator/>

		This is
		<div sclass="@load(('lockIndicator ' += vm.lockable.status))">
			<choose>
				<when test="@load(vm.lockable.status eq 'OWNED')">
					<textbox value="@bind(vm.lockable.resource.value)"/>
				</when>
				<otherwise>
					<label value="@load(vm.lockable.resource.value)"/>
				</otherwise>
			</choose>
		</div>

		<apply template="@load(vm.lockable.status)">

			<template name="OWNED">
				I am the owner. No one except me can edit until I
				<button iconSclass="z-icon-unlock" label="unlock" onClick="@command('unlock')"/> it.
				...
			</template>

			<template name="AVAILABLE">
				No one has currently locked it. I can
				<button iconSclass="z-icon-lock" label="lock" onClick="@command('lock')"/> it.
				...
			</template>

			<template name="UNAVAILABLE">
				currently locked by '${vm.lockable.owner}'.
				...
			</template>

		</apply>
	</div>
</zk>
  • Line 14: dynamically @load the sclass'-property changing with the lock status
  • Lines 16,19: render the resource editable for status OWNED (using the shadow elements <choose>/<when>/<otherwise>)
  • Lines 25,27,33,39: <apply> dynamic templates to update controls/information for all 3 different lock states

In the zul file above all the render decisions are based on the EL vm.lockable.status for now it's sufficient to know that this status property will "somehow" contain and update the current lock status (synchronized for all users).

SimpleLockableViewModel

Also the view model doesn't contain a lot of logic:

SimpleLockableViewModel.java

public class SimpleLockableViewModel {
	private static final AtomicInteger userCounter = new AtomicInteger(0);
	private final String username = "User-" + userCounter.incrementAndGet();

	private final UiLockTracker<SimpleResource> lockTracker = new UiLockTracker<>(10, 5);
	private static SimpleResource sharedResource = new SimpleResource("the Resource Value");

	@Init
	public void init(@ContextParam(ContextType.DESKTOP) Desktop desktop) {
		desktop.enableServerPush(true);
		lockTracker.observe(new MvvmLockable<SimpleResource>(getUsername(), sharedResource) {
			@Override
			public void onLockEvent(LockEvent event) {
				super.onLockEvent(event);
				BindUtils.postNotifyChange(null, null, sharedResource, "value");
			}
		});
	}

	@Command
	public void lock() { lockTracker.lock(); }

	@Command
	public void unlock() { lockTracker.unlock(); }

	public Lockable<SimpleResource> getLockable() { return lockTracker.getLockable(); }

	public String getUsername() { return username; }
}

Worth mentioning is line 10 - enabling server-push as a prerequisite to enable UI updates from background threads.

Besides the usual @Command-bindings and getters, the only things remaining are the UiLockTracker (line 5) and Lockable/MvvmLockable (line 11,26) classes, explained later. SimpleResource is a trivial Java-bean with a single property: value - Since Lockable/UiLocktracker are generic this can be any arbitrary type.

Lockable/MvvmLockable

In order to "calculate" the lock status the Lockable needs a resource, the current user (self) and the lock owner information. Owner changes are indicated with a LockEvent whenever onLockEvent() is called.

Lockable.java

public class Lockable<T> {
	private final String self;
	private final T resource;
	private String owner;

	public Lockable(String self, T resource) {
		this.self = self;
		this.resource = resource;
	}

	protected void onLockEvent(LockEvent event) {
		this.owner = event.getOwner();
	}

	public String getSelf() { return self; }

	public T getResource() { return resource; }

	public String getOwner() { return owner; }

	public LockStatus getStatus() {
		return owner == null ? LockStatus.AVAILABLE :
				self.equals(owner) ? LockStatus.OWNED : LockStatus.UNAVAILABLE;
	}

	protected Object getResourceKey() {
		... generates a unique key for the resource
	}
}

By overriding the onLockEvent method UI updates can be implemented (especially when using MVC).

When using MVVM the changing properties can be notified automatically as in the implementation below.


MvvmLockable.java

public class MvvmLockable<T> extends Lockable<T> {
	...
	@Override
	public void onLockEvent(LockEvent event) {
		super.onLockEvent(event);
		BindUtils.postNotifyChange(null, null, this, "owner");
		BindUtils.postNotifyChange(null, null, this, "status");
	}
	...

Additional notifications of vm properties can be implemented by overriding the onLockEvent as needed.

	new MvvmLockable<SimpleResource>(getUsername(), sharedResource) {
		@Override
		public void onLockEvent(LockEvent event) {
			super.onLockEvent(event);
			BindUtils.postNotifyChange(null, null, sharedResource, "value");
		}
	}

Notifying the sharedResource.value property will make sure the rendered resource value is also updated in all observing browsers after a lock event. This especially allows conditional notification for different requirements in your UI.

So far we haven't seen how those LockEvents are triggered and distributed among the observing clients. Let's get into the details.

UiLockTracker

The UiLockTracker is surely the core class of this example - providing a convenient way to observe LockEvents on a Lockable, which we have already seen in the code above following the simple life cycle:

  1. observe - start observing a resource by providing a Lockable implementation to receive LockEvents
  2. lock - request a lock on a Lockable
  3. unlock - release the lock
  4. reset - stop observing the lockable

Between 1 and 4 it is possible receive and handle LockEvents for that resource.

Usage

	private final UiLockTracker<SimpleResource> lockTracker = new UiLockTracker<>(10, 5);
	...

		lockTracker.observe(new MvvmLockable<SimpleResource>(getUsername(), sharedResource) {
			@Override
			public void onLockEvent(LockEvent event) {
				super.onLockEvent(event);
				BindUtils.postNotifyChange(null, null, sharedResource, "value");
			}
		}
  • line 1: Create a UiLockTracker instance (the parameters are explained in the next paragraph)
  • line 4: Observe the sharedResource with the current user
  • line 6: Override onLockEvent to handle LockEvents

It provides the methods lockTracker.lock/unlock to request/release a lock - when succeeding those will emit the related LockEvents asynchronously resulting in calls to onLockEvent for each observing user.

Since we already use the MvvmLockable class the UI will update automatically by calling @load-bindings on component properties or shadow elements to re-apply templates.

Once the resource doesn't need to be observed any more a call to lockTracker.reset() performs the cleanup (not used in this simple example - it only uses a single object).

Implementation

Disclaimer: This implementation doesn't intend to be fully complete or stable - it also won't work in a clustered environment.

The goal of this article is not to explain how to perform 100% water tight resource locking, but to build a UI for the user to observe a resource and its status avoiding user error (not implementation error). So I'll only cover the ZK specific parts. If you are interested in RxJava2 please refer to the official documentation.

Internally a BehaviorSubject is used for each observed Lockable - which ensures each subscriber receives the last cached and subsequent LockEvents.

	private void subscribeToLockEvents(Lockable<T> lockable) {
		lockEventSubscription = LockService.observeResource(lockable.getResourceKey())
				.doOnNext(event -> System.out.println("locked: " + event.owner + " on " + event.getResourceKey()))
				.doOnNext(event -> toggleAliveCheck(event, lockable))
				.compose(ZkObservable.activated())
				.subscribe(lockable::onLockEvent, this::resetOnError);
	}
  • line 2: create/join the resource subject for the resource key
  • line 5,6: call onLockEvent inside an activated execution to allow UI updates

The composable ZkObservable.activated() ObservableTransformer is already explained in my previous article on DZone - it activates/deactivates the current desktop for modification allowing asynchronous UI updates via server push.

The sequence - observe -> lock -> unlock -> reset - is a straight forward implementation as long as things are under control.

There are certain occasions when things go wrong:

  • user forgets to release the lock (while the session times out)
  • user closes/refreshes the browser tab
  • the network disconnects
  • the browser process crashes or terminates

For such cases UiLockTracker implements 2 automatic cleanup mechanisms to ensure a resource becomes available again and observers are removed from the RX Subjects to avoid memory leaks.

DesktopCleanup

1. A DesktopCleanup listener is dealing with the common scenario - where a user closes/refreshes the browser tab and the case where the session times out due to inactivity.

	private void addDesktopCleanup() {
		removeDesktopCleanup();
		desktopCleanup = dt -> this.reset();
		desktop.addListener(desktopCleanup);
	}

	private void removeDesktopCleanup() {
		if(desktopCleanup != null) {
			desktop.removeListener(desktopCleanup);
			this.desktopCleanup = null;
		}
	}

line 3: calls reset() automatically when the desktop is destroyed.

This is enough for users NOT owning a lock since it doesn't really impact other users even if they keep observing until their session times out. However for users holding a lock a second mechanism is in place.

Alive-check

2. An alive-check to actively check whether the connection to a user owning a lock is still responding during the time they own the lock.

The implementation uses a second Observable to poll the client in configurable intervals by activating/deactivating the desktop for an instant of time.

The interval (10sec) and timeout (5sec) are configured when creating the lockTracker instance.

	private final UiLockTracker<SimpleResource> lockTracker = new UiLockTracker<>(10, 5);

The implementation tries to activate the desktop after each interval. This triggers a client-side round trip without any UI changes - just making sure the client is still responding.

	private void startAliveCheck() {
		System.out.println("alive check: started, " + lockable);
		aliveCheckSubscription = Observable.interval(aliveCheckInterval, TimeUnit.SECONDS)
				.compose(ZkObservable.activated())
				.timeout(aliveCheckTimeout + aliveCheckInterval, TimeUnit.SECONDS)
				.subscribe(
						aliveCount -> {
							System.out.println("alive check: " + (aliveCount + 1) * aliveCheckInterval + "sec, " + lockable);
						},
						err -> {
							System.err.println("Unlocking unresponsive lock owner: " + lockable + " - " + err);
							unlock();
						});
	}

	private void stopAliveCheck() {
		if(aliveCheckSubscription != null) {
			System.out.println("alive check: stopped, " + lockable);
			aliveCheckSubscription.dispose();
			aliveCheckSubscription = null;
		}
	}
  • line 3: start a periodic Observable to perform alive check with interval()
  • line 5: timeout-operator triggers an exception when activation takes too long


When an error occurs (e.g. when the timeout is exceeded or desktop activation fails) unlock() is called to make the resource available again.

LockService

This simplified dummy back-end implementation is based on RxJava. It keeps a map of BehaviorSubjects (one for each resourceKey). Each subject will emit LockEvents to inform all subscribers of the current lock owner. Subscribers decide how to handle the events accordingly.

The LockService will also check/close idle Subjects in fixed intervals (currently 20 seconds - hard coded)

This implementation doesn't perform any sanity checks, simply the last call to lock/unlock will determine the next event. It is not the goal of this example to implement transaction safety.

Key is: The UI has to reflect every decision the back-end makes. Whatever the reason (e.g. due to higher privileges), the UI has to react in some way (reflecting the latest state), following the single source of truth principle - to give the user an up-to-date view of the application state.

For this example I decided it's better to inform the user as early as possible than letting him edit a resource and telling him at the end, that he can't save due to a conflict.

Another UI implementation might decide to keep an item editable (even after losing the lock) - it will then have to deal with possible save/merge conflicts later. Your back-end will hopefully inform you about (and help you with) potential merge conflicts (Optimistic vs Pessimistic locking).

A more complex UI example

Using the same UiLockTracker and Lockable objects you can build a more complex UI such as the following inventory example.

It shows how varying resources can be observed/locked on demand and gives some ideas how to give obvious feedback to the observer. Both visualizing the LockState and enabling only the allowed interactions. If one InventoryItem is already locked/edited the UI remains responsive allowing to view/edit a different InventoryItem.

InventoryViewModel

InventoryViewModel.java

public class InventoryViewModel {
	private static final AtomicInteger userCounter = new AtomicInteger(0);
	private final String username = "user-" + userCounter.incrementAndGet();

	private ListModelList<InventoryItem> inventory;
	private final UiLockTracker<InventoryItem> lockTracker = new UiLockTracker<>(10, 5);

	@Init
	public void init(@ContextParam(ContextType.DESKTOP) Desktop desktop) {
		desktop.enableServerPush(true);
		inventory = new ListModelList<>(InventoryService.getInventory());
	}

	@Command
	public void view(@BindingParam("item") InventoryItem item) {
		lockTracker.reset();
		lockTracker.observe(new MvvmLockable(username, item));
		BindUtils.postNotifyChange(null, null, this, "currentItem");
	}

	@Command
	public void edit(@BindingParam("item") InventoryItem item) {
		view(item);
		lockTracker.lock();
	}

	@Command
	public void save(@BindingParam("item") InventoryItem item) {
		lockTracker.unlock();
		inventory.notifyChange(item);
	}

	@Command
	public void cancel(@BindingParam("item") InventoryItem item) {
		lockTracker.unlock();
	}

	public ListModelList<InventoryItem> getInventory() { return inventory; }

	public Lockable<InventoryItem> getCurrentItem() { return lockTracker.getLockable(); }

	public String getUsername() { return username; }
}
  • line 16,17: observes a Lockable<InventoryItem> only when viewed (resets previous items before)

Other than that this is still a trivial view model with @Command handler methods and getters.

inventory.zul

inventory.zul

<?import zk.example.template.locker.lockable.LockStatus?>
<?style src="/locker/template/lockableEditor.css"?>
<?component name="lockableEditor" templateURI="/locker/template/lockableEditor.zul"?>
<zk>
	<div viewModel="@id('vm') @init('zk.example.template.locker.InventoryViewModel')">
		You are user: ${vm.username}
		<grid model="@init(vm.inventory)">
			<template name="model">
				<row>
					<label value="@init(each.id)"/>
					<label value="@init(each.name)"/>
					<label value="@init(each.value)"/>
					<label value="@init(each.quantity)"/>
					<button label="view" iconSclass="z-icon-eye" onClick="@command('view', item=each)"/>
					<button label="edit" iconSclass="z-icon-edit" onClick="@command('edit', item=each)"/>
				</row>
			</template>
		</grid>

		<if test="@load(!empty vm.currentItem)">
			<lockableEditor lockable="@load(vm.currentItem)" editCommand="edit" saveCommand="save" cancelCommand="cancel">
				<template name="content">
					<apply template="@load(lockable.status)" item="@init(lockable.resource)">
						<template name="AVAILABLE" src="/locker/template/inventoryItemView.zul"/>
						<template name="UNAVAILABLE" src="/locker/template/inventoryItemView.zul"/>
						<template name="OWNED" src="/locker/template/inventoryItemEdit.zul"/>
					</apply>
				</template>
			</lockableEditor>
		</if>
	</div>
</zk>

line 3,21: declare/use a custom <lockableEditor> element referencing an external template

line 24-26: render item based on lock status using inventoryItemView.zul or inventoryItemEdit.zul

The lockableEditor template can be defined/styled once and reused for different viewable/editable objects in an application (Persons, Addresses, etc.)

lockableEditor.zul

This template wraps lockable resources by surrounding the editor with contextual information/controls based on the current LockStatus.

lockableEditor.zul

<zk>
	<groupbox>
		<caption>
			<label value="@load((lockable.resource += ' - ' += lockable.status))"/>
			<choose>
				<when test="@load(lockable.status eq LockStatus.UNAVAILABLE)">
					<span sclass="z-icon-lock" style="color: red"/>
					locked by:
					<label value="@load(lockable.owner)"/>
				</when>
				<when test="@load(lockable.status eq LockStatus.OWNED)">
					<span sclass="z-icon-lock" style="color: green"/>
					locked by you
				</when>
				<otherwise/>
			</choose>
		</caption>

		<div sclass="editorContent">
			<apply template="content"/>
			<apply template="@load(lockable.status)">
				<template name="AVAILABLE">
					<div sclass="availableOverlay" onClick="@command(editCommand, item=lockable.resource)"/>
				</template>
				<template name="OWNED">
					<div>
						<separator/>
						<button label="save" onClick="@command(saveCommand, item=lockable.resource)"/>
						<button label="cancel" onClick="@command(cancelCommand, item=lockable.resource)"/>
					</div>
				</template>
				<template name="UNAVAILABLE">
					<div sclass="unavailableOverlay"/>
				</template>
			</apply>
		</div>
	</groupbox>
</zk>
  • line 20: here the actual content is injected

After adding some CSS the result could look like this http://localhost:8080/locker/inventory.zul

Template-examples-locker-inventory.png

Summary

Resource locking is an interesting but complex topic. Since the chances of parallel access are often small a minimal approach as explained above might already cover the majority of cases - without impact on the data source.

I invite you to play with the example, develop your own thoughts on the techniques used. I hope the example leaves room for your own creative idea on how to communicate the current lock status to the user (be it dynamic CSS or dynamic components/templates).

BTW: These techniques are not limited to locking only, you could observe any other information (prices, availability, booking reservations ...) and respond with UI changes accordingly. When using MVVM the principle remains the same: data changes trigger UI updates.

If you like the article or have ideas to improve this approach let me know in the comments below.

Example Sources

The code examples are available on github in the zk-template-examples repository

zul files: https://github.com/zkoss-demo/zk-template-examples/tree/master/src/main/webapp/locker
java classes: https://github.com/zkoss-demo/zk-template-examples/tree/master/src/main/java/zk/example/template/locker

Running the Example

Clone the repo

   git clone git@github.com:zkoss-demo/zk-template-examples.git

The example war file can be built using the gradle-wrapper (on windows simply omit the prefix './'):

   ./gradlew war

Execute using gretty:

   ./gradlew appRun

Execute using jetty-runner (faster startup):

   ./gradlew startJettyRunner

Then access the example http://localhost:8080/zk-template-examples/locker/


Comments



Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License.