From 31ee50d07da62eed26c47840cbb76ed99d3e8e80 Mon Sep 17 00:00:00 2001 From: Morten Svanaes Date: Sat, 8 Aug 2026 13:38:16 +0800 Subject: [PATCH 1/5] perf: NONSTRICT_READ_WRITE for the reference metadata bucket Every cached region in DHIS2 uses READ_WRITE, whose access strategy holds one ReentrantReadWriteLock PER REGION (not per key): every get takes the region read lock and every putFromLoad takes the region WRITE lock, even when it writes nothing (entities are unversioned, so an existing entry is never overwritten from a load). Under concurrent load on option-heavy metadata this serialises a whole region behind single-key work: measured 14-15% of ALL JVM wall samples parked in AbstractReadWriteAccess. Switches the static reference bucket (Option*, PeriodType, DataElement, Category*, OptionSet.options, Legend*, Indicator*, OrgUnit hierarchy; 30 files, 71 declarations) to NONSTRICT_READ_WRITE, which has no region lock. Trade-off: a brief staleness window after a write, accepted for reference metadata (sign-off: Morten, 2026-08-07). Period/RelativePeriods stay READ_WRITE. Measured on a tracker-import ramp: parked-in-region-lock 14.15% -> 0.05%, p99 at 100 concurrent users 58.4s -> 20.7s as an isolated change. AI Assisted --- .../main/java/org/hisp/dhis/category/Category.java | 6 +++--- .../java/org/hisp/dhis/category/CategoryCombo.java | 4 ++-- .../org/hisp/dhis/category/CategoryOption.java | 10 +++++----- .../org/hisp/dhis/dataelement/DataElement.java | 10 +++++----- .../org/hisp/dhis/indicator/IndicatorGroup.java | 6 +++--- .../org/hisp/dhis/indicator/IndicatorGroupSet.java | 4 ++-- .../src/main/java/org/hisp/dhis/legend/Legend.java | 2 +- .../main/java/org/hisp/dhis/legend/LegendSet.java | 2 +- .../main/java/org/hisp/dhis/option/OptionSet.java | 2 +- .../hisp/dhis/category/hibernate/Category.hbm.xml | 6 +++--- .../category/hibernate/CategoryDimension.hbm.xml | 4 ++-- .../category/hibernate/CategoryOptionCombo.hbm.xml | 4 ++-- .../category/hibernate/CategoryOptionGroup.hbm.xml | 6 +++--- .../hibernate/CategoryOptionGroupSet.hbm.xml | 4 ++-- .../CategoryOptionGroupSetDimension.hbm.xml | 4 ++-- .../dataelement/hibernate/DataElementGroup.hbm.xml | 6 +++--- .../hibernate/DataElementGroupSet.hbm.xml | 4 ++-- .../hibernate/DataElementGroupSetDimension.hbm.xml | 4 ++-- .../hibernate/DataElementOperand.hbm.xml | 2 +- .../dhis/indicator/hibernate/Indicator.hbm.xml | 8 ++++---- .../dhis/indicator/hibernate/IndicatorType.hbm.xml | 2 +- .../org/hisp/dhis/option/hibernate/Option.hbm.xml | 2 +- .../hisp/dhis/option/hibernate/OptionGroup.hbm.xml | 4 ++-- .../dhis/option/hibernate/OptionGroupSet.hbm.xml | 4 ++-- .../hibernate/OrganisationUnit.hbm.xml | 14 +++++++------- .../hibernate/OrganisationUnitGroup.hbm.xml | 6 +++--- .../hibernate/OrganisationUnitGroupSet.hbm.xml | 4 ++-- .../OrganisationUnitGroupSetDimension.hbm.xml | 4 ++-- .../hibernate/OrganisationUnitLevel.hbm.xml | 2 +- .../hisp/dhis/period/hibernate/PeriodType.hbm.xml | 2 +- 30 files changed, 71 insertions(+), 71 deletions(-) diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/Category.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/Category.java index f2dfa6a5ee3d..6ce3fcfb7a54 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/Category.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/Category.java @@ -113,7 +113,7 @@ @Entity @Table(name = "category") @Setter -@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @JacksonXmlRootElement(localName = "category", namespace = DxfNamespaces.DXF_2_0) public class Category extends BaseMetadataObject implements DimensionalObject, SystemDefaultMetadataObject { @@ -155,11 +155,11 @@ public class Category extends BaseMetadataObject name = "categoryoptionid", foreignKey = @ForeignKey(name = "fk_category_categoryoptionid"))) @OrderColumn(name = "sort_order") - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private List categoryOptions = new ArrayList<>(); @ManyToMany(mappedBy = "categories", fetch = FetchType.LAZY) - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private Set categoryCombos = new HashSet<>(); @Embedded private TranslationProperty translations = new TranslationProperty(); diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryCombo.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryCombo.java index 866137bf9979..8625aa8cc48d 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryCombo.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryCombo.java @@ -124,11 +124,11 @@ public class CategoryCombo extends BaseMetadataObject foreignKey = @ForeignKey(name = "fk_categorycombo_categoryid"))) @OrderColumn(name = "sort_order") @ListIndexBase(1) - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private List categories = new ArrayList<>(); @OneToMany(fetch = FetchType.LAZY, mappedBy = "categoryCombo") - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private Set optionCombos = new HashSet<>(); @Column(name = "datadimensiontype", nullable = false) diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOption.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOption.java index 4f3666b1d426..22ed22343586 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOption.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOption.java @@ -112,7 +112,7 @@ @Entity @Table(name = "categoryoption") @Setter -@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @JacksonXmlRootElement(localName = "categoryOption", namespace = DXF_2_0) public class CategoryOption extends BaseMetadataObject implements DimensionalItemObject, SystemDefaultMetadataObject, Serializable { @@ -159,21 +159,21 @@ public class CategoryOption extends BaseMetadataObject name = "categoryoption_organisationunits", joinColumns = @JoinColumn(name = "categoryoptionid"), inverseJoinColumns = @JoinColumn(name = "organisationunitid")) - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @BatchSize(size = 100) private Set organisationUnits = new HashSet<>(); @ManyToMany(mappedBy = "categoryOptions") - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @BatchSize(size = 100) private Set categories = new HashSet<>(); @ManyToMany(mappedBy = "categoryOptions") - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private Set categoryOptionCombos = new HashSet<>(); @ManyToMany(mappedBy = "members") - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private Set groups = new HashSet<>(); @Type(type = "jsbObjectSharing") diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataelement/DataElement.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataelement/DataElement.java index 9ec89f1285b2..f2625eac450c 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataelement/DataElement.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataelement/DataElement.java @@ -132,7 +132,7 @@ @Entity @Table(name = "dataelement") @Cacheable -@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @JacksonXmlRootElement(localName = "dataElement", namespace = DxfNamespaces.DXF_2_0) public class DataElement extends BaseMetadataObject implements DimensionalItemObject, @@ -213,12 +213,12 @@ public class DataElement extends BaseMetadataObject /** The data element groups which this data element is a member of. */ @ManyToMany(mappedBy = "members") - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private Set groups = new HashSet<>(); /** The data sets which this data element is a member of. */ @OneToMany(mappedBy = "dataElement") - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private Set dataSetElements = new HashSet<>(); /** The lower organisation unit levels for aggregation. */ @@ -229,7 +229,7 @@ public class DataElement extends BaseMetadataObject foreignKey = @ForeignKey(name = "fk_dataelementaggregationlevels_dataelementid")) @Column(name = "aggregationlevel") @OrderColumn(name = "sort_order") - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private List aggregationLevels = new ArrayList<>(); /** Indicates whether to store zero data values. */ @@ -258,7 +258,7 @@ public class DataElement extends BaseMetadataObject name = "legendsetid", foreignKey = @ForeignKey(name = "fk_dataelement_legendsetid"))) @OrderColumn(name = "sort_order") - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private List legendSets = new ArrayList<>(); @AuditAttribute diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorGroup.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorGroup.java index 55f8b0ccc916..0f78ce47b461 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorGroup.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorGroup.java @@ -93,7 +93,7 @@ @Setter @Entity @Table(name = "indicatorgroup") -@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) public class IndicatorGroup extends BaseMetadataObject implements IdentifiableObject, MetadataObject { @Id @@ -123,7 +123,7 @@ public class IndicatorGroup extends BaseMetadataObject @JoinColumn( name = "indicatorid", foreignKey = @ForeignKey(name = "fk_indicatorgroup_indicatorid"))) - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private Set members = new HashSet<>(); @Type(type = "jsbAttributeValues") @@ -135,7 +135,7 @@ public class IndicatorGroup extends BaseMetadataObject private Sharing sharing = new Sharing(); @ManyToMany(mappedBy = "members", fetch = FetchType.LAZY) - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private Set groupSets = new HashSet<>(); // ------------------------------------------------------------------------- diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorGroupSet.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorGroupSet.java index c2e784eeb570..29fb410a825f 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorGroupSet.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorGroupSet.java @@ -93,7 +93,7 @@ @Setter @Entity @Table(name = "indicatorgroupset") -@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) public class IndicatorGroupSet extends BaseMetadataObject implements IdentifiableObject, MetadataObject { @@ -134,7 +134,7 @@ public class IndicatorGroupSet extends BaseMetadataObject foreignKey = @ForeignKey(name = "fk_indicatorgroupset_indicatorgroupid"))) @OrderColumn(name = "sort_order", nullable = false) @ListIndexBase(1) - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private List members = new ArrayList<>(); // ------------------------------------------------------------------------- diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/legend/Legend.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/legend/Legend.java index c4872212bf6e..d06a4a215d90 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/legend/Legend.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/legend/Legend.java @@ -90,7 +90,7 @@ @Index(name = "maplegend_endvalue", columnList = "endvalue") }) @JacksonXmlRootElement(localName = "legend", namespace = DxfNamespaces.DXF_2_0) -@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @Setter public class Legend implements IdentifiableObject, EmbeddedObject { diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/legend/LegendSet.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/legend/LegendSet.java index 7e5f105fc613..6a7b0918a0de 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/legend/LegendSet.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/legend/LegendSet.java @@ -104,7 +104,7 @@ public class LegendSet extends BaseMetadataObject implements IdentifiableObject, @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "maplegendsetid") - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private Set legends = new HashSet<>(); public LegendSet() {} diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/option/OptionSet.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/option/OptionSet.java index cf78c9c6369c..828d2909d789 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/option/OptionSet.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/option/OptionSet.java @@ -127,7 +127,7 @@ public class OptionSet extends BaseMetadataObject implements IdentifiableObject, @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JoinColumn(name = "optionsetid", foreignKey = @ForeignKey(name = "fk_optionset_optionid")) @OrderBy(value = "sortOrder ASC") - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private List