Guides
March 11, 2021 · 8 min read

Syncing 10,000 options with a Jira customfield

batch process with optimized performance via the Customfield Editor for Jira App REST-API

We already showed in our previous blogpost on how to sync your external database values with a Jira customfield. In this blogpost we want to focus on the performance side of syncing external values for customfields with around 10,000 options.

Note customfields with above 1000 options can already have negative impact on performance.

Test infrastructure

For our test infrastructure we choose the cost-effectiveness and optimal performance Jira L recommendation of Atlassian. Once you have over 800 customfields the Jira L instance size is recommended for optimal performance.

Recommendations for cost-effectiveness and optimal performance - Jira L
Jira version8.13 LTS
Customfield Editor
for Jira version
2.9.1 (contains performance optimizations)
3x Application NodesAWS EC2 c5.2xlarge = 8 vCPU and 16 GiB RAM
1x Database NodeAWS RDS m4.xlarge = 4 vCPU and 16 GiB RAM
Postgres 11.10

We have setup this environment with the AWS Quick Starts CloudFormation templates for Jira. All EC2 and RDS instances run with SSD harddrives.

Make sure CloudFormation has created your instances and you have scaled up correctly to three nodes.

And also make sure that the SettingsSystemClustering page shows that all your cluster nodes are up and active. You should always check the SettingsSystemTroubleshooting and support page to check that the cluster caches are in sync.

Test data and import script

Our Jira instance has 1,500 customfields and we will insert 10,000 options at once.

The import script is written in NodeJS and inserts 10,000 random options and waits 100ms after each insert.

First we init a NodeJS project and install the dependencies

$
npm init -y && npm install axios unique-names-generator

Then we need to create the sync-script as sync.js file.

sync.js
const axios = require("axios");
const { uniqueNamesGenerator, adjectives, colors, animals } = require("unique-names-generator");

const jiraContext = {
  jiraBaseUrl: "https://jira-test-ax2421s.codeclou.com", // without trailing slash
  customFieldId: 10336,
  contextId: 10436,
  username: "admin",
  password: "adminpw",
};

const generateRandomOptionname = () => {
  return uniqueNamesGenerator({ dictionaries: [adjectives, colors, animals] });
};

const getBaseUrlAndAuthForAxios = (jiraContext) => {
  return {
    restBaseUrl: `${jiraContext.jiraBaseUrl}/rest/jiracustomfieldeditorplugin/1`,
    axiosOptions: {
      auth: {
        username: jiraContext.username,
        password: jiraContext.password,
      },
    },
  };
};

const createCustomfieldOption = async (jiraContext, optionValue) => {
  const { restBaseUrl, axiosOptions } = getBaseUrlAndAuthForAxios(jiraContext);
  try {
    const res = await axios.post(
      `${restBaseUrl}/user/customfields/${jiraContext.customFieldId}/contexts/${jiraContext.contextId}/options`,
      { optionvalue: optionValue },
      axiosOptions
    );
    return res;
  } catch (err) {
    console.log(
      "unexpected error during createCustomfieldOption",
      err && err.response && err.response.data ? err.response.data : err
    );
  }
};

const getCurrentMilliSeconds = () => {
  // this is more precise as new Date()
  var hrTime = process.hrtime();
  return Math.floor(hrTime[0] * 1000 + hrTime[1] / 1000000);
};

const main = async () => {
  try {
    const beforeBatch = getCurrentMilliSeconds();
    for (let counter = 0; counter < 10000; counter++) {
      const before = getCurrentMilliSeconds();
      const optionValue = generateRandomOptionname() + "-" + counter;
      await createCustomfieldOption(jiraContext, optionValue);
      const after = getCurrentMilliSeconds();
      console.log(`creating option ${optionValue} took ${after - before} ms`);
      // Wait 100ms after each REST API Call to give Jira time to internally process data
      await new Promise((resolve) => setTimeout(resolve, 100));
    }
    const afterBatch = getCurrentMilliSeconds();
    console.log("BATCH TOOK MS:");
    console.log(afterBatch - beforeBatch);
  } catch (err) {
    console.log("unexpected error", err);
  }
};
main();

Now we can execute the sync-script with NodeJS.

$
node sync.js

The script prints the overall time it took in milliseconds at the end. You can expect it to run 1.5 hours.

During the run we manually deleted and updated options on the same field and context to increase the load and to measure the response time of those operations.

Import script performance results

The whole batch run took 110 minutes to create 10,000 options.

The environment on AWS was located in Oregon (us-west-2) and our machine that imported the data was located in Germany (therefore a higher network latency).

operationaverage during runaverage after run
create option650ms80ms
update option250ms60ms
delete option10,500ms10,000ms
sort options-10,000ms

Import script recommendations

General performance recommendations


Appendix: Jira configs

Should you not use the AWS CloudFormation template the following Jira config files might be interesting for you.

The database config dbconfig.xml of Jira is autogenerated by the AWS CloudFormation template.

<?xml version="1.0" encoding="UTF-8"?>

<jira-database-config>
  <name>defaultDS</name>
  <delegator-name>default</delegator-name>
  <database-type>postgres72</database-type>
  <schema-name>public</schema-name>
  <jdbc-datasource>
    <url>jdbc:postgresql://jiradb.c23e.us-west-2.rds.amazonaws.com:5432/jira</url>
    <username>atljira</username>
    <password>Password123</password>
    <driver-class>org.postgresql.Driver</driver-class>

    <pool-min-size>20</pool-min-size>
    <pool-max-size>20</pool-max-size>
    <pool-min-idle>10</pool-min-idle>
    <pool-max-idle>20</pool-max-idle>

    <pool-max-wait>10000</pool-max-wait>
    <validation-query>select 1</validation-query>
    <time-between-eviction-runs-millis>60000</time-between-eviction-runs-millis>
    <min-evictable-idle-time-millis>180000</min-evictable-idle-time-millis>
    <pool-remove-abandoned>true</pool-remove-abandoned>
    <pool-remove-abandoned-timeout>60</pool-remove-abandoned-timeout>
    <pool-test-while-idle>true</pool-test-while-idle>
    <pool-test-on-borrow>false</pool-test-on-borrow>
  </jdbc-datasource>
</jira-database-config>

This is a reflection of what we entered during the CloudFormation setup (defaults).

Same goes for the setenv.sh which is also auto generated and sets 12 GB of memory on each cluster node. Leaving 4 GB of memory free to the system for background tasks.

JVM_SUPPORT_RECOMMENDED_ARGS="-XX:+ExplicitGCInvokesConcurrent -XX:ReservedCodeCacheSize=512M"
#  You can use variable below to modify garbage collector settings.
#  For Java 8 we recommend default settings
#  For Java 11 and relatively small heaps we recommend: -XX:+UseParallelGC
#  For Java 11 and larger heaps we recommend: -XX:+UseG1GC -XX:+ExplicitGCInvokesConcurrent
JVM_GC_ARGS="-XX:+ExplicitGCInvokesConcurrent"
# The following 2 settings control the minimum and maximum given to the JIRA Java virtual machine. 
JVM_MINIMUM_MEMORY="12288m"
JVM_MAXIMUM_MEMORY="12288m"
# The following setting configures the size of JVM code cache. 
# A high value of reserved size allows Jira to work with more installed apps.
JVM_CODE_CACHE_ARGS="-XX:InitialCodeCacheSize=32m -XX:ReservedCodeCacheSize=512m"


Summary

To summarize - your Jira instance should be up-to-date to run versions that include performance optimizations for customfields. This means running Jira 8.13+ and Customfield Editor for Jira 2.9.1+.

Lastly make sure your Jira instance meets the correct size recommendations of Atlassian.