Caching Strategies in Configuration

Please jump to more advanced subjects at the end if you already know caching basics.

Caching Basics

It is common in programming to have repeated operations inside a loop, or similar needs where an expensive computation has been made and should be kept temporarily so the result can be shared to other places in the code.

Consider the following elements in a price list logic, which queries a Datamart for SUM of NetSales for the product:

GetData.groovy

def ctx = api.getDatamartContext() def dm = ctx.getDatamart("Transactions") def q1 = ctx.newQuery(dm, true) q1.select("sku") q1.select("SUM(NetSales)", "SumNetSales") q1.where(Filter.equal("sku", api.product("sku"))) return ctx.executeQuery(q1).getData()

PreviousSales.groovy

return out.GetData?.getAt(api.product("sku"))

 

Imagine that this price list was created for all products in the system. This structure would work, but it will result in a new database connection and a new query for every product. This is very inefficient.

If we removed the filter on line 8, it would still work, but the query would return results for ALL products, not just the current product.

To solve this, we still remove the filter on line 8, but instead of returning the result immediately, we store it in a cache somewhere for future - so it doesn’t have to be calculated again.

Something like:

if(!cache["NetSalesData"]){ def ctx = api.getDatamartContext() def dm = ctx.getDatamart("Transactions") def q1 = ctx.newQuery(dm, true) q1.select("sku") q1.select("SUM(NetSales)", "SumNetSales") //q1.where(Filter.equal("sku", api.product("sku"))) //dont need this any more cache["NetSalesData"] = ctx.executeQuery(q1).getData() } return cache["NetSalesData"]

The cache object here could be api.local / api.global, or some custom caching object.

Using this approach, the query executes once, and stores all results in memory so that future references to this element can just return the same result - instead of re-executing the query. Much better!

Refer to below sections on some ideas how to use caching.

In-Memory Caches (api.local / api.global)

Refer to the CacheUtil section below for better examples of usage.

api.local

As mentioned in API Documentation, api.local is “a hash map that is available during the lifetime of exactly one logic execution. It can (and should) be used to store intermediate results that need to be accessed by other subsequent logic elements.

This means api.local lives in a row-level context. In a price list, this means a single product. This means that api.local can store anything you like until the end of the logic for this row (product).

api.local is not able to pass data from one row to another.

api.local has no TTL (Time to Live), as it has a specific lifetime scope as defined above.

api.global

As mentioned in API Documentation, “by setting api.retainGlobal = true you can instruct the formula engine to keep the (api.global) hash map in between logic runs. It works between line items and in case of Quotes, Agreements/Promotions, Rebate Agreements and Claims, between the header and the line items (but only in the pre-phase, not in the post-phase). The values are also carried forward between two list calculation passes.”

Simply put, api.global is an in-memory hashmap that exists between rows / line items (products), exists between multiple calculation passes, and exists between header pre-phase to line item.

Using this, you can cache something in api.global and it can be passed to more places – but will cost more memory until the process is finished and that RAM is released.

api.global has no TTL, as it has a specific lifetime scope as defined above.

Distributed (Shared) Cache

Refer to the CacheUtil section for better examples of usage.

 

api.getSharedCache & api.setSharedCache are the methods for using the Pricefx shared cache. The shared cache is a distributed key-value store (distributed cache), usually powered by https://redis.io/. Shared cache is isolated per-partition. It does not support passing data between partitions unless specially configured to do so.

This cache is accessible to all logics, and so can be very powerful – but also very confusing. It is possible to put something into the shared cache from somewhere like a dashboard logic, then it can be read from somewhere totally different like a price list logic.

The shared cache has a TTL (Time to Live) for every item added to it. Basically, this is the amount of time the cache will keep an item before clearing it. The TTL is refreshed every time a key is accessed, or set, in the shared cache. By default, the TTL is 15 minutes – but this can be changed in cluster configurations by Support team.

Note: The shared cache can only store strings. This makes it inefficient to store large objects, as they have to be JSON serialized & deserialized every time they’re read or written.

CacheUtil & Examples

In GitLab, there is a shared library element CacheUtils for much easier use of these caching methods.

Below are some examples of how to use the util.

In all examples, the provided function will run, and the result will be stored in the desired cache. If the function returns something and populates the cache, every subsequent call will return the result without re-executing the function.

By default, NULL values will not cache, and will result in re-execution until result is returned (unless otherwise specified).

Memoization (@Memoized)

References:

Use with great caution!

Memoization is an advanced method of caching, and should be rarely used. Memoization is most useful for resource-expensive computation like heavy recursive functions, that will always return the same result based on the input parameters.

Consider:

In this example, Memoization is bad, because the data inside the Datamart might change over time.

 

Consider:

In this example, it makes sense to use Memoized because the result will always be the same based on the given parameters.

Note: There is no way to expire a memoized cached result (it requires server reboot).

Found an issue in documentation? Write to us.