How to cache a rest endpoint? (Groovy Scriptrunner)

Italo Qualisoni [e-Core]
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
October 21, 2017

Hello!

We have a script rest endpoint that each time that it's called get an information from an external database.

We would like to cache this function since that multiple times the result query returns the same value.

Looking in the internet we have found someways with Guava framework (https://github.com/google/guava) but we would like your help to see if is there any example to this need.

 

What do you recommend ?

Thanks!

1 answer

1 accepted

2 votes
Answer accepted
Gregor Kasmann_Actonic
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
October 22, 2017
Italo Qualisoni [e-Core]
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
October 23, 2017

Not yet, this looks promissor. I'll give it a try!

Italo Qualisoni [e-Core]
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
October 23, 2017

@Gregor Kasmann_Actonic,

I'm trying to to use the provided document.

It seems that each time that I call  CacheManager.getCache function the cache is erased and created a new one.

I'm testing using the Script Console (feature of Scriptrunner)

import com.atlassian.cache.Cache;
import com.atlassian.cache.CacheLoader;
import com.atlassian.cache.CacheManager;
import com.atlassian.cache.CacheSettingsBuilder;
import com.atlassian.util.concurrent.NotNull;
import com.atlassian.cache.CacheSettings;
import java.util.concurrent.TimeUnit;
import com.atlassian.jira.component.ComponentAccessor;

def CacheManager cacheManager = ComponentAccessor.getComponent(CacheManager.class);

def cache = cacheManager.getCache("com.foo.test.cache",
new ListPagesCacheLoader(),
new CacheSettingsBuilder().expireAfterAccess(5, TimeUnit.MINUTES as TimeUnit).build());

log.error cache.getKeys()
cache.get("hi")
log.error cache.getKeys()
cache.get("hi")
log.error cache.getKeys()

public class ListPagesCacheLoader implements CacheLoader<String, String> {
@Override public String load(@NotNull String value){
sleep(3000);
return value;
}
}

 image.png

Italo Qualisoni [e-Core]
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
October 23, 2017

@Gregor Kasmann_Actonic,

It seems that the cache is being recreated each time that I call CacheManager#getCache

We need this cache stored in disk or memory to each time that the rest endpoint be called we still have the keys into it.

Do you have any tip ?

import com.atlassian.cache.Cache;
import com.atlassian.cache.CacheLoader;
import com.atlassian.cache.CacheManager;
import com.atlassian.cache.CacheSettingsBuilder;
import com.atlassian.util.concurrent.NotNull;
import com.atlassian.cache.CacheSettings;
import java.util.concurrent.TimeUnit;
import com.atlassian.jira.component.ComponentAccessor;

def CacheManager cacheManager = ComponentAccessor.getComponent(CacheManager.class);

def cache = cacheManager.getCache("com.foo.test.cache",
new ListPagesCacheLoader(),
new CacheSettingsBuilder().expireAfterAccess(5, TimeUnit.MINUTES as TimeUnit).build());

log.error cache.getKeys()
cache.get("hi")
log.error cache.getKeys()
cache.get("hi")
log.error cache.getKeys()

public class ListPagesCacheLoader implements CacheLoader<String, String> {
@Override
public String load(@NotNull String value){
sleep(3000);
return value;
}
}

image.png

Gregor Kasmann_Actonic
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
October 24, 2017

As I can see on your example, cache works properly:

2017-10-24 10_25_13-How to cache a rest endpoint_ (Groovy Scriptrunner....png

Italo Qualisoni [e-Core]
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
October 24, 2017

Yes, it did work :)

But if I run the same script again the expected output should get data from cache. But the actual output is the same and I keep getting the first call with 3 seconds delay.

I'm afraid that when I call the getCache method the cache is being flushed.

def cache = cacheManager.getCache("com.foo.test.cache",
new ListPagesCacheLoader(),
new CacheSettingsBuilder().expireAfterAccess(5, TimeUnit.MINUTES as TimeUnit).build());

Do you know how can I define the cache in a way that I can run multiple times the same script without flush the cache's keys? Maybe this happens because I'm running within the script console, this behavior might be different when writing a P2 addon...

Thanks for your help!

Maybe the below picture helps to understand my issue:

image.png

Italo Qualisoni [e-Core]
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
October 26, 2017

Never mind, I've just tested with Script Endpoint and the cache worked just fine! :D

 

Thanks!!!!!!!!!!

Erik Macias June 4, 2020

Hi @Italo Qualisoni [e-Core]

I wrote a similar test as you have above (for confluence), and noticed the same thing, where the first output of cache.getKeys() is empty. Did you have to do something to resolve this? Or is it a limit of testing via scriptrunner's script console?

 

cc @Gregor Kasmann_Actonic 

Italo Qualisoni [e-Core]
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
June 9, 2020

@Erik Macias ,

 In my case I was indeed creating a new cache with the same key and therefore my cache was flushed. There is another way to retrieve the cached values that I found later.

can you share your test with us? I would like to check and provide you a better answer

Like Erik Macias likes this
Erik Macias June 9, 2020

Hi @Italo Qualisoni [e-Core]

Thank you for responding, especially on a 3 year old question :)

I actually created a question myself that I'll link here for more details. But below is my codebase. 


import groovy.json.JsonSlurper
import org.apache.commons.lang3.StringUtils;
import com.atlassian.cache.Cache;
import com.atlassian.cache.CacheLoader;
import com.atlassian.cache.CacheManager;
import com.atlassian.cache.CacheSettings;
import com.atlassian.cache.CacheSettingsBuilder;
import com.atlassian.spring.container.ContainerManager;
import com.atlassian.util.concurrent.NotNull;
import com.atlassian.plugin.spring.scanner.annotation.imports.ConfluenceImport;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import org.apache.log4j.Logger
import org.apache.log4j.Level


class AppDeploymentsService {
private final Cache<String, Object> cache;
def url
def log

AppDeploymentsService() {
url = new PropertyService('appDeployments').get("app_deployment_url")
}

AppDeploymentsService(def blank) {
def cacheManager = (CacheManager)
ContainerManager.getComponent("cacheManager");
cache = cacheManager.getCache(this.class.getName() + ".cache",
new AppDeploymentsServiceCacheLoader(), new
CacheSettingsBuilder().expireAfterAccess(30,
TimeUnit.MINUTES).build())
url = new PropertyService('appDeployments').get("app_deployment_url")
log = Logger.getLogger(this.class.getName()) log.setLevel(Level.DEBUG)
}

// The loader class used to populate a cache entry on cache miss.
private class AppDeploymentsServiceCacheLoader implements
CacheLoader<String, Object> {
@Override public Object load(@Nonnull String cacheKey) {
return getApplicationInfo(cacheKey)
}
}

def getApplicationInfo(applicationId) {
def uri = "${url}/${applicationId}/deployments"
return new JsonSlurper().parse(new Rest(uri).get())
}

def getAppDeploymentInfo(applicationId) {
// if the appId is not present in the cache then the
// AppDeploymentsServiceCacheLoader
// will create the value as implemented in the 'load' method
// and it will be populated into the cache automatically.
log.debug "cache keys before: ${cache.getKeys()}"
def ret = cache.get(applicationId);
log.debug "cache keys after: ${cache.getKeys()}"
//return evaluate(ret)
return ret
}
}

In case the formatting is off, I will attach a screen shot of my code
script-editor.png

 

Regards,
Erik Macias

Erik Macias June 9, 2020

Duplicated post from above. Removing duplicate. 

Italo Qualisoni [e-Core]
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
June 10, 2020

I'm glad to help someone that is having a similar issue that I had in the past :)

Every time you create AppDeploymentsService object, your code will initialize your cache. For the first run that's is fine but when you need to reuse the cache instead of use your existing cache you are replacing the cache with a new empty cache (line 28 in your code).

Also something I noticed during my test is that after the AppDeploymentsService object has been used , the cache seems to be flushed. So I tested using Singleton pattern and it seems to works as expected, I'll share with you now what to change:

1. Add a new attribute in your AppDeploymentsService class:

static AppDeploymentsService appDeploymentsService;

2. Add a new method that will return your singleton object:

public static AppDeploymentsService getAppDeploymentsService(){
if(this.appDeploymentsService == null){
this.appDeploymentsService = new AppDeploymentsService("blank");
}
return this.appDeploymentsService;
}

3. From now on, you should use AppDeploymentsService.getAppDeploymentsService() instead of new getAppDeploymentsService("blank")

See my screenshots here with the output:

scriptrunner-confluence.png

PS: I have some issues when I updated the script editor but the console was not reflecting the changes, so I had to use the Scriptrunner Built-in "Clear Groovy class loader", I recommend you to use this whenever you change your AppDeploymentsService groovy;

cache-clear.png

Hope this helps you,
Italo Qualisoni

Like Erik Macias likes this
Italo Qualisoni [e-Core]
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
June 26, 2020

@Erik Macias 

Did it work?

Leo October 11, 2021

@Italo Qualisoni [e-Core] 

Hi, just wanted to say thank you - i had that exact same problem and solved it thanks to you.

Cheers,

Leo

Suggest an answer

Log in or Sign up to answer