One more reason to avoid global variables

v2pos5d.jpg

You can find tons of articles and discussions on the web why global variables should be avoided in any programming language. Here is another one that is AX specific.

Recently, I had to deal with a heisenbug and when I got to the bottom of it, I realized that anyone could do the same rookie mistake. I did a quick search and found at least one in standard application, so it’s worth to share and maybe you need to fix it in your code as well!

This bug is related to SysGlobalObjectCache. It is recommended to use it for better performance. To create an entry in the cache we need scope, key and value. Usually we use fixed scope for each task type (i.e. CustVendExternalItemDescriptionExistsCheck). Key usually is a record Id we are searching in DB (i.e. “0001”) and value is a data returned from DB (i.e. InventTable record).  But we often forget that records in different legal entities could have same id and result cached in one company is not valid for another company!

Let’s look at an example. On standard demo data I found an item that has same id (0001) in 2 companies (usrt, frrt). I added external description for it only in one company (usrt). Now I’m using standard findExternalDescription() method of VendExternalItemDescription class that returns external item description. Under the hood it checks if there is a record for given item and vendor combination and caches the result to avoid extra database calls.

In the test job bellow first call to findExternalDescription() method caches false because there is no description for given key. Then I switch to the company where I have a description, but method still returns false because there is an entry in the cache. When the cache is cleared method finally returns true because it does actual search in DB.

class RunnableClass1
{
    public static void main(Args _args)
    {
        ItemId itemId = '0001';
        VendAccount vendAccount = '1001';
        inventDimId inventDimId = 'AllBlank';

        changecompany('frrt')
        {
            //search for an item description. False is cached because no description exists
            boolean found = new VendExternalItemDescription(itemId, InventDim::find(inventDimId), vendAccount).findExternalDescription();
            info(queryValue(found));
        }

        //change company to company where description exists
        changecompany('usrt')
        {
            boolean found = new VendExternalItemDescription(itemId, InventDim::find(inventDimId), vendAccount).findExternalDescription();
            //false is returned because it is cached from previous call
            info(queryValue(found));

            //reset cache
            SysGlobalObjectCache::clearAllCaches();

            found = new VendExternalItemDescription(itemId, InventDim::find(inventDimId), vendAccount).findExternalDescription();
            //true is returned after cache flush
            info(queryValue(found));
        }
    }
}

As you can see, it’s very easy to create similar issue or maybe you already have one, because it is data specific and we rarely test our customizations using multiple companies. From another side, it is even easier to fix it, just add DataAreaId to the cache key!

Advertisements

AX 7. Clear Extension Framework cache from UI.

Extension Framework uses attribute to instantiate descendants and you need to flush cache after creating or removing an attribute. There is a blog post how to do this from code, but we have a way to do the same from UI (and it’s quite important because you cannot go and create a job in new AX anymore to flush your cache after deployment).

Go to Transportation management->Setup->Load building->Load building strategies form and click on Generate class list button. Under the hood AX will call SysFlushAOD::main() to clear all caches.

GenerateClassList.jpg

This button will clear various of different caches, not only  Extension Framework one, so could be used in different scenarios! Kudos to my fellow colleague Alexey Borisov for pointing this out!