Teneo Developers

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.

This approach means that the owning StaticDataExample can modify the implementation from static property to getter method to lazy loaded as desired with no changes needed to the scripts using it.

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.
  • 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.

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().
  • 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.

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.

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.

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

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

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.