100 Days of Code – Day #2 – Threading vs Async in ScriptRunner

Threading is a fantastic and (relatively) simple way to run parallel HTTP requests against a Jira or Confluence instance.  Multithreading can drastically cut down the length of time it takes for a piece of code to run, as each HTTP request is not waiting for the previous request to finish.  In Groovy we can achieve threading by using the Thread class or the ExecutorService framework.

However, threading isn’t a viable solution when it comes to Jira (or Confluence) on the Cloud.   Because Jira Cloud is a shared hosting environment, there are a number of reasons why Atlassian has put strict limitations on the use of threading.    Chief among these concerns are performance and security.   If anyone can run any number of threads that they want, this necessarily impacts the other users of the shared hosting environment.

Similarly, multithreading in a shared hosting environment can cause data inconsistencies and synchronization issues, which easily lend themselves to visions of major security issues.

 

Though we cannot use threading on Jira Cloud, we can use async.  Simply put, the difference between the two is that threading uses concurrent or parallel threads of execution to achieve concurrency.  That is, it literally splits the tasks into separate requests to the CPU. 

Async, on the other hand, uses non-blocking code to achieve concurrency.   That is, the script simply says “okay, we’re going to run this part of the script in the background, and when it’s done we’ll collect the results.”  Async still technically uses threads, but it uses them in a way that is different than the ExecutorService.   The material difference between the two is that threading creates new threads, which uses up resources.  Async uses whatever threads are available from the existing thread pool.   What’s important to know is that the Atlassian Cloud supports async, and so that’s what we use.

 

Let’s look at an example. 

The process to actually declare an asynchronous method isn’t terribly complicated. The script below essentially does two things:  it creates HTTP connections using an array of URLs, and it adds those tasks to a collection of asynchronous tasks.  When the tasks have each completed, the results are added to a string buffer.  When all the async tasks have completed, the contents of the string buffer are returned.

The process of creating the HTTP requests isn’t new or complicated, we do that all the time when working with the REST API.   The only complication here is understanding the method that starts the future tasks, and understanding the structure of the loop that runs the requests.

 

import java.net.HttpURLConnection
import java.net.URL
import groovy.transform.CompileStatic
import java.util.concurrent.FutureTask

@CompileStatic
def async (Closure close) {
  def task = new FutureTask(close)
  new Thread(task).start()
  return task
} //Tell Groovy to use static type checking, and define a function that we use to create new async requests

String username = "<username>"
String password = "<password>"
//Define the credentials that we'll use to authenticate against the server/DC version of Jira or Confluence
//If we want to authenticate against Jira or Confluence Cloud, we'd need to replace the password with an API token, and replace the username with an email address

// Define a list of URLs to fetch
def urls = [
  "<URL>",
  "<URL>",
  "<URL>",
  "<URL>",
  "<URL>",
]

// Define a list to hold the async requests
def asyncResponses = []

def sb = []
// Loop over the list of URLs and make an async request for each one

urls.each {
  url ->
    //For each URL in the array

    def asyncRequest = {
      //Define a new async request object

      HttpURLConnection connection = null
      //Define a new HTTP URL Connection, but make it null

      try {
        // Create a connection to the URL
        URL u = new URL(url)
        connection = (HttpURLConnection) u.openConnection()
        connection.setRequestMethod("GET")
        //Create a new HTTP connection with the current URL, and set the request method as GET

        connection.setConnectTimeout(5000)
        connection.setReadTimeout(5000)
        //Set the connection parameters

        String authString = "${username}:${password}"
        String authStringEncoded = authString.bytes.encodeBase64().toString()
        connection.setRequestProperty("Authorization", "Basic ${authStringEncoded}")
        //Set the authentication parameters

        // Read the response and log the results
        def responseCode = connection.getResponseCode()
        def responseBody = connection.getInputStream().getText()
        logger.warn("Response status code for ${url}: ${responseCode}")
        sb.add("Response body for ${url}: ${responseBody}")
      } catch (Exception e) {
        // Catch any errors
        logger.warn("Error fetching ${url}: ${e.getMessage()}")
      } finally {
        // Terminate the connection
        if (connection != null) {
          connection.disconnect()
        }
      }
    }
  asyncResponses.add(async (asyncRequest))
}

// Wait for all async responses to complete
asyncResponses.each {
  asyncResponse ->
    asyncResponse.get()
}

return sb

Leave a Reply

Your email address will not be published. Required fields are marked *