Last updated Mar 27, 2024

What is it

JiraThreadLocalUtil is an interface located in com.atlassian.jira.util.thread, used for setting up and clearing ThreadLocal variables. It is implemented by JiraThreadLocalUtilImpl class. Any plugin which creates its own threads for processing must use this component. Otherwise, it can interfere with the smooth running of Jira by creating data inconsistencies, leaking resources and affecting performance.

Known issues

Performance

Running threads without JiraThreadLocalUtil can cause a major hit in Jira’s performance. It mostly manifests itself in slowness while performing specific actions, for example running plugin features or reindexing. Which action is affected varies from case to case, depending on the code which is executed by the thread.

Inconsistencies

Threads changing Jira data can outcome unexpected results. For example, if a thread is updating an issue, it can be incorrectly indexed or moved to a different scrum board. Moreover, it can break some Jira’s functions. In some cases full reindexing won't be executed properly.

How to use it properly

Let’s see how to use JiraThreadLocalUtil.

public class SampleConcurrentUpdater {

private static final Logger log = Logger.getLogger(SampleConcurrentUpdater.class);

private final ThreadPoolExecutor executor;

public SampleConcurrentUpdater() { this.executor = new ThreadPoolExecutor( 1, 1, 300L, TimeUnit.MINUTES, new ArrayBlockingQueue<>(), new ThreadFactoryBuilder() .setNameFormat("SampleConcurrentUpdater:thread - %d") .setDaemon(true) .build()); }

1
2
    public void updateIssue(String issueKey) throws IndexException {
        executor.submit(() -> updateIssueStatus(issueKey));
    }
    /*...*/
}

In this sample, we create a simple class which passes a updateIssueStatus() method to ThreadPoolExecutor. Running a thread in such a way can cause previously mentioned problems. To eliminate this risk, we use JiraThreadLocalUtil:

1
2
public class SampleConcurrentUpdater  {
    private static final Logger log = Logger.getLogger(SampleConcurrentUpdater.class);

    private final ThreadPoolExecutor executor;
    private final JiraThreadLocalUtilImpl jiraThreadLocalUtil;

    public SampleConcurrentUpdater() {
        this.jiraThreadLocalUtil = new JiraThreadLocalUtilImpl();
        this.executor = new ThreadPoolExecutor(
               1, 1, 300L, TimeUnit.MINUTES,
               new ArrayBlockingQueue<>(2),
                new ThreadFactoryBuilder()
                        .setNameFormat("example-MyPluginComponentImpl:thread - %d")
                        .setDaemon(true)
                        .build());
    }

    public void updateIssue(String issueKey) throws IndexException {
        executor.submit(() -> {
            jiraThreadLocalUtil.preCall();
            try
            {
                updateIssueStatus(issueKey);
            }
            finally
            {
                jiraThreadLocalUtil.postCall(
                    log, 
                    () -> log.error("postCall did not clean up properly"));
            }
        });
    }
}

We use JiraThreadLocalUtilImpl, which contains all the ready-to-use methods needed to safely run threads. The code passed to the thread needs to be in the following order:

1
2
public void run(){
    jiraThreadLocalUtil.preCall();
    try{
        // do runnable code here
    }
    finally{
    jiraThreadLocalUtil.postCall(log, myWarningCallback);
}

You may wonder what those methods do. The method preCall() sets up a clean ThreadLocal environment for the code. The method postCall() clears up that ThreadLocal after the code has finished. We pass a logger, and a callback method myWarningCallback which will be executed if there are any problems during the cleaning process. This method can be null, it will log those problems as errors.

Rate this page: