Commit 5f6a524
fix: avoid loading UserRole members during JSON Patch apply [DHIS2-21852] (#24489)
* fix: avoid loading UserRole members during JSON Patch apply
JsonPatchManager converted the managed entity to a tree via valueToTree
and then re-invoked every collection getter in handleCollectionUpdates.
For UserRole, getUsers() initializes all lazy members, causing
O(members) SQL and hundreds of MB of allocation on scalar PATCHes.
Skip non-owner collection properties that no patch path references, in
both serialization passes. Non-owner collections are ignored by
metadata import UPDATE, so omitting them is semantically free; owner
collections always stay in the tree (omitting them would clear them on
import). Non-owner collections referenced by patch paths keep today's
behavior.
* test: add UserRoles PATCH performance simulation
Gatling simulation patching a scalar field on an empty control role and
on a large-membership role (platform-perf DB), making the O(members)
PATCH regression visible. No calibrated thresholds yet; asserts 100%
success only.
* fix: skip non-persisted derived props during JSON Patch serialize
OrganisationUnit.leaf calls children.isEmpty(), which initializes the
inverse children collection even when JsonPatchManager already omits
non-owner collections. Exclude unreferenced non-persisted properties
from patch serialization so derived getters cannot force lazy loads.
* test: OU scalar JSON patch must not init inverse collections
* test: User scalar JSON patch skips userGroups, keeps userRoles
* test: UserGroup scalar JSON patch keeps owner users
* test: DataElementGroup name patch preserves owner members
* test: web-api JSON patch side-effect coverage across entities
* refactor: rework JsonPatch collection-exclusion filter to match FieldFilterService pattern [DHIS2-21852]
Addresses review feedback on #24489 (Jan Bernitt): the per-call `.addMixIn(realClass, ...)` +
inline mixin + filter-provider wiring in JsonPatchManager.apply() was rebuilt from scratch on every
call and didn't follow any existing convention. Reworks it to mirror the codebase's established
pattern for the same underlying problem -- skipping a getter during Jackson serialization based on
per-call criteria, without invoking it -- already used for the `?fields=` GET path
(org.hisp.dhis.fieldfiltering.FieldFilterService/FieldFilterMixin).
- New `JsonPatchFilterMixin` (`@JsonFilter`) and `JsonPatchExcludedPropertyFilter`
(`SimpleBeanPropertyFilter`), shaped like `FieldFilterMixin`/`FieldFilterSimpleBeanPropertyFilter`.
- `JsonPatchManager` caches one mixin-bound `ObjectMapper` copy per entity type
(`ConcurrentHashMap<Class<?>, ObjectMapper>`, built lazily via `computeIfAbsent`) instead of
rebuilding the mixin binding on every `apply()` call. `findExcludableNonOwnerCollections` (the
exclusion rule itself) is unchanged -- it was already schema-driven and generic.
- The mixin is scoped per-`realClass`, not bound to `Object.class` globally: `FieldFilterMixin`'s own
filter is path-aware, so a global binding is safe for it, but `JsonPatchExcludedPropertyFilter`
matches by property name only. A global binding was tried first and found, in review, to silently
strip `Sharing.users`/`Sharing.userGroups` (present on every `IdentifiableObject`) whenever a
same-named top-level collection was excluded, e.g. on a scalar UserRole PATCH. Scoping per-class
makes that cross-type collision impossible. Regression test:
`testUserRoleSharingUsersSurviveScalarPatch`.
- Answers Jan's second question (should an explicit patch to an excluded property throw?): no --
`findExcludableNonOwnerCollections` already un-excludes a property referenced by a patch path, so
it falls back to full (slower, correct) hydration rather than being silently dropped. Replaced the
previous `testUserRoleUsersPathPatchDoesNotThrow` (asserted only `assertNotNull`, would pass either
way) with `testUserRoleUsersPathPatchFallsBackToFullHydration`, which asserts
`Hibernate.isInitialized(...)` actually flips to `true`, mutation-tested against the fallback logic.
No functional change to PATCH behavior beyond fixing the Object.class regression introduced and
caught within this same review cycle (never released). All existing JsonPatchManagerTest coverage
(14 tests) plus the new JsonPatchExcludedPropertyFilterTest (2 tests) pass; UserControllerTest (52
tests, dhis-test-web-api) unaffected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* style: spotless JsonPatchSideEffectControllerTest
* test: calibrate UserRolesPerformanceTest p95/max thresholds
Calibrated from the baseline/candidate A/B run on the platform-perf DB used to
recalibrate PR #24489 after the rebase onto the JsonPatchFilterMixin refactor.
PATCH scenarios share one threshold since the invariant under test is that
PATCH latency must not depend on role membership size.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* refactor: make JsonPatch filter and mixin package-private [DHIS2-21852]
Per review: both are internal to JsonPatchManager and not reused outside
the package, so drop public visibility.
---------
Co-authored-by: Jason Pickering <jason@dhis2.org>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>1 parent c53415d commit 5f6a524
8 files changed
Lines changed: 1115 additions & 3 deletions
File tree
- dhis-2
- dhis-services/dhis-service-core/src
- main/java/org/hisp/dhis/jsonpatch
- test/java/org/hisp/dhis/jsonpatch
- dhis-test-integration/src/test/java/org/hisp/dhis/jsonpatch
- dhis-test-performance/src/test/java/org/hisp/dhis/test/platform
- dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller
Lines changed: 88 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
Lines changed: 51 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/jsonpatch/JsonPatchManager.java
Lines changed: 121 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| 39 | + | |
39 | 40 | | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
40 | 46 | | |
41 | 47 | | |
42 | 48 | | |
| |||
59 | 65 | | |
60 | 66 | | |
61 | 67 | | |
| 68 | + | |
62 | 69 | | |
63 | 70 | | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
64 | 94 | | |
65 | 95 | | |
66 | 96 | | |
| |||
73 | 103 | | |
74 | 104 | | |
75 | 105 | | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
76 | 116 | | |
77 | 117 | | |
78 | 118 | | |
| |||
86 | 126 | | |
87 | 127 | | |
88 | 128 | | |
89 | | - | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
90 | 137 | | |
91 | 138 | | |
92 | 139 | | |
93 | 140 | | |
94 | | - | |
| 141 | + | |
95 | 142 | | |
96 | 143 | | |
97 | 144 | | |
| |||
100 | 147 | | |
101 | 148 | | |
102 | 149 | | |
103 | | - | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
104 | 217 | | |
105 | 218 | | |
106 | 219 | | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
107 | 225 | | |
108 | 226 | | |
109 | 227 | | |
| |||
0 commit comments