api.global versus api.local

Introduction

The api.global and api.local are hashmaps populated initially empty in the memory of each server pod. They were designed for caching to store database query results so the queries are not repeatedly called again and again or to reduce queries count. Map api.local is initialized before calculation of every line item and is kept during execution of all elements while api.globalmap is initialized only once before the calculation starts and is kept during execution of all elements for all line items.

The use of these maps can lead to bad code, therefore these maps should be used only for queries caching and follow the best practices, see the next section.

If the cache was built in order to join the data from another table, consider use of QueryAPI. This will typically lead to a better performing as well as better readable code.

Best practice

Like mentioned above, both functions were designed to store cached values from queries. Use for anything else should be avoided to minimum since preading api.global and api.local across the elements and functions logic will lead to bad readable and maintainable code.

Do not use api.global and api.local variables to bypass the arguments of functions. This is like using global variables which is in general a bad practice in programming.

Writing

It is recommended to write cache value entry in a dedicated function (e.g. findCountries()) or in dedicated elements (e.g. “Global”, “Batch” etc.).

List findCountries() {
    api.global.computeIfAbsent("Countries") {
        def qapi = api.queryApi()

        def t1 = qapi.tables().companyParameterRows("Country")

        return qapi.source(t1)
                .stream {
                    it.collectEntries {
                        [(it.Country): it.CountryName]
                    }
                }
    }
}

You can quickly get a similar sample code snippet to the above by using a Live template in Studio - simply type pfxGlobalCache <ENTER> in the groovy editor or console.

Reading

It is also recommended to read the cache values from a dedicated function like above or create a dedicated element. So e.g. instead of reading api.global.Countries directly consider creation of an element “Countries” with Display=none which will only return api.global.Countries. Then access the cached countries using out.Countries.

If you are using a dedicated element, make sure you set ‘Display’ to ‘None'. See Avoid Returning Big Data in Element Result With Display Mode

api.global

The formula engine keeps the api.global hashmap in between logic runs in 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 (i.e., form initial run to dirty item run; ONLY in non-distributed mode.).

Remember that for distributed calculations api.global is populated on each server pod and each thread independently. So if you store there the results of some data queries, those queries will get executed on every server pod (and not once as one may think).

You should always:

  • Cache some reasonable amount of data to be used for multiple items. Storing big data causes bad performance when recalculating a single item.
    info  Since Vesper 6.0, the maximum string size of JSON serialized api.global content that is passed on to dirty passes is limited to 2MB. This is configurable in pricefx-config.xml (maxGlobalsSize).

  • Define a constant to be used in many elements of the logic.

When api.global should not be used:

  • For passing values to functions. These functions would later become difficult to use. Consider passing values to a function as regular function parameters.

Use of api.global requires enabled setting in Administration > Configuration > General > ‘api.retainGlobal defaults to TRUE’. This is set by default for all projects since Manhattan 4.0 release (2019).

For old projects with partitions created before Manhattan 4.0 release:

  1. Ideally set the above ‘api.retainGlobal defaults to TRUE' setting. Don’t forget to enable this in all partitions.

  2. In case it would not be possible, set the variable api.retainGlobal = true in the very first element of the logic; otherwise the value of api.global will get lost which leads to performance issues. This used to be a common error that caused performance issues because values that should be cached for all items were calculated for every item.

api.local

Before using api.local, consider creating a new element (possibly with the Display option = None) and store the value there. Performance-wise it is identical since both out.Element and api.local are represented by HashMaps. You will also debug the value more easily: as elements result(s) of a logic test. You also will not need to call api.trace to debug the value. Last but not least, it will also help you avoid potential naming clash of the api.local key.

So api.local should be used only in the following scenarios:

  • If you set api.local in some element and you then need to change the value in some other element (try to avoid it though).

  • You need to store multiple calculated values and you do not want to return Maps as an element result. For example, you calculate a price at certain place and want to set a price reason text at once. In that case, the element typically returns the price and sets the price reason as e.g., api.local.reason.

When api.local should not be used:

  • For passing values to functions. These functions would later become difficult to use. Consider passing values to a function as function parameters.

Calculation Flows Specifics

api.global persists between Calculation Flow (CF) logic executions and is stored within the globalFormulaState property of the CF object. If you do not want to share the global cache between logic executions, use the api.local instead, or add api.retainGlobal = false at the beginning of CF logics.
Note: api.global data in Calculation Flows (in the globalFormulaState property) is stored as a String. Use the jsonDecode(String) to convert it back to the Groovy object (a Map).

Example

Groovy
 def cf = api.find(
     "CF",
     Filter.equal("uniqueName", "ScheduleCalculations"),
     Filter.equal("draft", "false")
 ).first()

 def config = api.jsonDecode(cf.configuration).entries.get(0)?.globalFormulaState

 return config.lastCalculationDate

Usage examples are documented at Caching Strategies in Configuration.

If any of the variables api.global or api.local holds a lot of data, these variables should not be returned as an element result since the data gets usually persisted for each calculated record.