Race condition in custom fields
The JIRA Service Desk development team recently discovered a race condition that impacts the AbstractSingleFieldType (The issue in question can be seen here: https://jira.atlassian.com/browse/JSDSERVER-5299). If a thread needs to update a value, it retrieves all existing values before adding a new row and then finally deleting the old rows leaving the new value. If, between the add and the delete another thread is called then it will retrieve 2 rows, but the AbstractSingleFieldType can only handle one value so one of the rows is discarded. The issue was resolved in JSD Server 3.6.4 but to summarize:
- When a thread is updating the value of a custom field, it retrieves all existing values, adds a new row containing the new value and then discards the old value.
- if between the add and delete, another thread gets called, then there will be two rows rather than one, customFieldValuePersister only handles 1 value.
- To resolve this, the first value is selected and all other values are deleted. These values are not explicitly ordered by date, therefore older values can be selected with more recent values being removed.
It’s worth noting that this problem impacts values which are calculated based off of the previous value in the field, other values which are calculated from first principles each time are not impacted by this issue. In order to address the problem the JSD development team took the following actions:
- Add an “updated” nullable column to the customfieldvalue table, so that results can be consistently ordered. This lets us select the newest value to keep in event of above race condition.
- the UPDATE into a Transaction. By putting these statements into a transaction, it isn’t committed until completed and this means that our race condition is less likely to occur in the first place.
There is a RequestCache in JIRA (jira.customfield.values.cache) that will store the last retrieved Issues value of your custom field anyway. If you’re relying on that fields data to consistently read from the database in your plugins, you’ll need to remove that entry from the JiraAuthenticationContextImpl.getRequestCache to ensure you actually will go to the database every time.