Lazy Loading
In some cases it is advantageous to not get everything at solution load time (to not add load time / memory footprint if the data is only sometimes needed) - but still to share the information across sessions, or within a session.
This can be achieved with "lazy loading" - that is: loading the data only at the point that it is first requested, therefore not needing to know exactly when the first request will be and pre-load the data.
Lazy loading can be implemented seamlessly from a user perspective with some Groovy goodness.
Define a private field and a getXXX
method which will:
- Return the field if it is populated.
- Populate it then return it if it is not yet populated.
groovy
1// Solution Loaded
2class StaticDataExample {
3 private static String _serverVersion
4 static String getServerVersion() {
5 return _serverVersion ?: (_serverVersion = "${serverLocation}/version".toURL().text)
6 }
7}
8
Which can be called as if it were a property by removing the get and lowercasing:
groovy
1// calls StaticDataExample.getServerVersion() under the hood
2if(StaticDataExample.serverVersion < 10) {
3 // Execute in legacy mode
4}
5
Groovy exposes all getSomethingOrOther
methods as properties: somethingOrOther.
The Lazy Loading Principle
Here the "magic" is in the getServerVersion
method where first the method checks to see if data is set - if it is not getter()
is called, and the value stored to data. The next call to getServerVersion
will check data, see it has a value, and therefore not call the getter:
- First call to
getServerVersion
.- Data is not set ("falsey": Groovy Truth) so
getter()
is called.
- Data is not set ("falsey": Groovy Truth) so
- Result stored to data.
- Second call to
getServerVersion
.- Data is set ("truthy") so getter() is not called and the existing value of data is returned.
Thread safe lazy loading (Recommended)
In the above simple code it is possible that 2 threads could both call getServerVersion at the same time - meaning that:
- Thread 1 checks: data is not set.
- So thread 1 calls
this.getter()
.
- So thread 1 calls
- Thread 2 checks (before thread 1 has called the getter and stored the value): data is not set.
- So thread 2 also calls
this.getter()
. Therefore 2 calls are made.
- So thread 2 also calls
Since sessions in Teneo run at "people speed" the above method is "good enough" in most Teneo scenarios to prevent multiple sessions causing multiple calls to the get. This is because script execution during a single session is guaranteed thread-safe, so the only way for multiple concurrent calls to a single lazy property is if 2 user sessions both get to that point at the same time. This is unlikely - but not impossible.
Lazy class
To completely prevent concurrent calls - and actually to simplify the code, eg. the recommended way - we can use a helper class "Lazy" to encapsulate the storing and one-time retrieval of the data
groovy
1// Solution Loaded
2class Lazy<T> {
3 private T data
4 private final Closure<T> getter
5
6 Lazy(Closure<T> getter) {
7 this.getter = getter
8 }
9
10 T getValue() {
11 T localData
12 synchronized(this.getter) {
13 localData = this.data ? this.data : (this.data = this.getter())
14 }
15 return localData
16 }
17}
18
The synchronized block in getValue around the test and get of the data ensures that only one thread can be inside that block at any time.
Shared data usage
Then use this class in the StaticDataExample to handle the version property:
groovy
1// Solution Loaded
2class StaticDataExample {
3 private static Lazy<String> _serverVersion = new Lazy<String>(() => "${serverLocation}/version".toURL().text)
4 static String getServerVersion() { return _serverVersion.value }
5}
6
Session data usage
For session scope data the same Lazy<T>
class can be used, but using non-static properties and an instance of the class as a global variable
groovy
1// Solution Loaded
2class SessionDataExample {
3 private Lazy<String> _user = new Lazy<String>(() => "${serverLocation}/user/${userId}/".toURL().text)
4 String getUserLevel() { return _user.value.level }
5}
6
7// Global variable `user` default value
8new SessionDataExample()
9
Flow scope data usage
If lazy loading is needed in a flow scope then a flow variable with a class defined the same way as the session data class can be used.