Skip to content

feat: decouple ChangeLogAccumulator from Hibernate Session [DHIS2-21378]#23836

Merged
enricocolasante merged 2 commits into
masterfrom
DHIS2-21378
May 12, 2026
Merged

feat: decouple ChangeLogAccumulator from Hibernate Session [DHIS2-21378]#23836
enricocolasante merged 2 commits into
masterfrom
DHIS2-21378

Conversation

@enricocolasante

@enricocolasante enricocolasante commented May 7, 2026

Copy link
Copy Markdown
Contributor

What changes

ChangeLogAccumulator

  • flushAll(EntityManager)flushAll(Connection).
    The class was already pure JDBC internally; it only used Hibernate to unwrap a Connection via session.doWork(). That indirection is removed.
    EntityManager and org.hibernate.Session imports are gone. The class now has zero Hibernate dependencies.

AbstractTrackerPersister

  • Injects DataSource as a constructor field (via @RequiredArgsConstructor).
  • At the start of persist(), obtains the connection once with DataSourceUtils.getConnection(dataSource) and releases it in a finally block via DataSourceUtils.releaseConnection(conn, dataSource).
    Within the active @Transactional context in DefaultTrackerBundleService.commit(), DataSourceUtils returns the connection already bound to the Spring-managed transaction — the same connection Hibernate's doWork was providing — so transaction integrity is maintained.
  • Passes conn to changeLogs.flushAll(conn) at both flush points (FlushMode.OBJECT inside the entity loop, FlushMode.AUTO after it).
  • SQLException from the FlushMode.AUTO flush path is caught at the outer level and wrapped in PersistenceException (unchecked), triggering Spring's transactional rollback. SQLException from the FlushMode.OBJECT path is caught by the per-entity inner catch, consistent with the existing atomic/non-atomic branching.

Concrete persisters (TrackedEntityPersister, EnrollmentPersister, TrackerEventPersister, SingleEventPersister, RelationshipPersister)

  • Each constructor gains a DataSource parameter forwarded to super(...).

Performance

Four compare runs of a load test only on the tracker importer

25659662711
25659660683
25656873606
25656346626

Median Response Time (p50) (ms)

Requests baseline req/s candidate req/s Diff (ms) Change
Login 122 0.38 117 0.38 -5 ⬇️ -4.1%
MNCH import 1,071 1.91 1,054 1.92 -18 ⬇️ -1.6%
Child Programme import 497 1.91 506 1.92 +10 ⬆️ +1.9%
ANC import 384 1.91 380 1.92 -4 ⬇️ -1.2%

95th Percentile Response Time (p95) (ms)

Requests baseline req/s candidate req/s Diff (ms) Change
Login 230 0.38 247 0.38 +17 ⬆️ +7.3%
MNCH import 1,271 1.91 1,330 1.92 +59 ⬆️ +4.7%
Child Programme import 552 1.91 555 1.92 +3 ⬆️ +0.6%
ANC import 457 1.91 441 1.92 -15 ⬇️ -3.3%

⬇️ = faster, ⬆️ = slower

Open question

In non-atomic mode with FlushMode.OBJECT, a SQLException from changeLogs.flushAll(conn) leaves the PostgreSQL connection in an aborted-transaction state. Subsequent SQL on the same connection fails with "current transaction is aborted", so a single flush failure silently cascades to all remaining entities in the batch. This becomes more significant in Phase 3 when EntityWriteBatch.flush(conn) is added alongside changeLogs.flushAll(conn).
When a SQLException is thrown, does it mean that there is a bug in the query? Should we just return 500?

What does not change

  • No behaviour change for callers.
  • DefaultTrackerBundleService and the @Transactional boundary are unchanged.

Next steps

  • Quick fixformatDate: SimpleDateFormatstatic final DateTimeFormatter
  • Phase 0 ✅ Remove EntityManager from TrackerPersister interface and all method parameters - feat: decouple ChangeLogAccumulator from Hibernate Session [DHIS2-21378] #23836
  • Phase 1 ✅ Decouple ChangeLogAccumulator from Hibernate Session; obtain the
    transaction-bound Connection via DataSourceUtils instead of session.doWork()
  • Phase 2 — Extract FileResource assignment update out of AbstractTrackerPersister
    into FileResourceStore
  • Phase 3 — Introduce EntityWriteBatch; accumulate and batch-flush TEAV writes
    (multi-row INSERT / unnest UPDATE / tuple DELETE)
  • Phase 4 — Pre-allocate IDs from PostgreSQL sequences; stage top-level entity
    inserts and updates in EntityWriteBatch; remove all entityManager.flush() calls
  • Phase 5 — Replace updateTrackedEntitiesLastUpdated named query with
    NamedParameterJdbcTemplate; remove last Hibernate dependency from
    DefaultTrackerBundleService
  • Phase 6 — Implement JDBC flush methods one entity type at a time
    (TrackedEntityEnrollmentTrackerEvent/SingleEventRelationship)

@sonarqubecloud

sonarqubecloud Bot commented May 8, 2026

Copy link
Copy Markdown

@enricocolasante
enricocolasante marked this pull request as ready for review May 11, 2026 09:08
@enricocolasante
enricocolasante requested a review from a team as a code owner May 11, 2026 09:08
@muilpp

muilpp commented May 11, 2026

Copy link
Copy Markdown
Contributor

When a SQLException is thrown, does it mean that there is a bug in the query? Should we just return 500?

IMO, it's either a bug in the query or infrastructure. It shouldn't be because of the data because at that point it's been validated already, so I think returning a 500 is fine.
That said, will be implement the save points mentioned in this comment?

@enricocolasante
enricocolasante requested a review from a team May 11, 2026 13:54
@enricocolasante

Copy link
Copy Markdown
Contributor Author

When a SQLException is thrown, does it mean that there is a bug in the query? Should we just return 500?

IMO, it's either a bug in the query or infrastructure. It shouldn't be because of the data because at that point it's been validated already, so I think returning a 500 is fine. That said, will be implement the save points mentioned in this comment?

I don't think so. That is just one consideration based on the answer of the open question. I agree with you that a 500 should be fine.

@enricocolasante
enricocolasante merged commit 0c1779f into master May 12, 2026
24 checks passed
@enricocolasante
enricocolasante deleted the DHIS2-21378 branch May 12, 2026 09:50
enricocolasante added a commit that referenced this pull request Jul 7, 2026
… (2.43) (#24286)

* feat: remove EntityManager from TrackerPersister [DHIS2-21378] (#23797)

* feat: remove EntityManager from TrackerPersister interface and method parameters [DHIS2-21378]

* Use default timezone

(cherry picked from commit 052d3ac)

* feat: decouple ChangeLogAccumulator from Hibernate Session [DHIS2-21378] (#23836)

(cherry picked from commit 0c1779f)

* chore: Extract FileResource assignment update [DHIS2-21378] (#23879)

* chore: Extract FileResource assignment update [DHIS2-21378]

* Code review fixes

* Code review fixes

(cherry picked from commit 83542f4)

* feat: stage TEAV writes through EntityWriteBatch [DHIS2-21378] (#23970)

(cherry picked from commit b30c1ce)

* feat: Batch entity when persisting [DHIS2-21378] (#24005)

(cherry picked from commit 100dff1)

* feat: remove EntityManager from DefaultTrackerBundleService [DHIS2-21378] (#24024)

(cherry picked from commit 3759453)

* feat: TrackedEntity JDBC insert [DHIS2-21378] (#24027)

(cherry picked from commit 4eaf6ba)

* feat: TrackedEntity JDBC update [DHIS2-21378] (#24079)

(cherry picked from commit d4af287)

* feat: Enrollment JDBC insert/update [DHIS2-21378] (#24081)

* feat: TrackedEntity JDBC update [DHIS2-21378]

* feat: Enrollment JDBC insert/update [DHIS2-21378]

* Remove L2 caching from enrollment

* Code review fixes

* Code review fixes

(cherry picked from commit 34d629e)

* feat: Tracker event JDBC insert/update [DHIS2-21378] (#24082)

* feat: TrackedEntity JDBC insert [DHIS2-21378]

* feat: TrackedEntity JDBC update [DHIS2-21378]

* feat: Enrollment JDBC insert/update [DHIS2-21378]

* feat: Tracker event JDBC insert/update [DHIS2-21378]

(cherry picked from commit 435872b)

* feat: Single event JDBC insert/update [DHIS2-21378] (#24083)

* feat: TrackedEntity JDBC insert [DHIS2-21378]

* feat: TrackedEntity JDBC update [DHIS2-21378]

* feat: Enrollment JDBC insert/update [DHIS2-21378]

* feat: Tracker event JDBC insert/update [DHIS2-21378]

* feat: Single event JDBC insert/update [DHIS2-21378]

* Format code

(cherry picked from commit 9703e4f)

* feat: Relationship JDBC insert/update [DHIS2-21378] (#24093)

* feat: TrackedEntity JDBC insert [DHIS2-21378]

* feat: TrackedEntity JDBC update [DHIS2-21378]

* feat: Enrollment JDBC insert/update [DHIS2-21378]

* feat: Tracker event JDBC insert/update [DHIS2-21378]

* feat: Single event JDBC insert/update [DHIS2-21378]

* feat: Relationship JDBC insert/update [DHIS2-21378]

(cherry picked from commit 658f0a8)

* feat: TEAV JDBC insert/update [DHIS2-21378] (#24098)

* feat: TrackedEntity JDBC update [DHIS2-21378]

* feat: Enrollment JDBC insert/update [DHIS2-21378]

* feat: Tracker event JDBC insert/update [DHIS2-21378]

* feat: Single event JDBC insert/update [DHIS2-21378]

* feat: Relationship JDBC insert/update [DHIS2-21378]

* feat: TEAV JDBC insert/update [DHIS2-21378]

* Code review fixes

* Fix tests

* Fix idScheme issue for TEAV

(cherry picked from commit 4e3367c)

* fix: Refactor persistence layer in tracker importer [DHIS2-21378] (#24197)

* feat: TrackedEntity JDBC update [DHIS2-21378]

* feat: Enrollment JDBC insert/update [DHIS2-21378]

* feat: Tracker event JDBC insert/update [DHIS2-21378]

* feat: Single event JDBC insert/update [DHIS2-21378]

* feat: Relationship JDBC insert/update [DHIS2-21378]

* feat: TEAV JDBC insert/update [DHIS2-21378]

* Code review fixes

* Fix tests

* Fix idScheme issue for TEAV

* fix: Refactor persistence layer in tracker importer [DHIS2-21378]

* Fix merge

(cherry picked from commit 194dcd1)

* fix: Harmonize db sequences for tracker [DHIS2-21378] (#24208)

* fix: Harmonize db sequences for tracker [DHIS2-21378]

* Fix relationshipitemid

(cherry picked from commit f52eefa)

* fix: Rename conflicting migration [DHIS2-21378] (#24218)

(cherry picked from commit 7aefc1c)

No-op on 2.43: this PR renamed V2_44_12 -> V2_44_13 to avoid a master-only
Flyway version clash. On 2.43 the migration is numbered V2_43_63 (its own
series), so no rename is needed. Empty commit kept to preserve the full
DHIS2-21378 backport history.

* refactor: Split EntityWriteBatch into per-entity writers [DHIS2-21378] (#24227)

* refactor: Split EntityWriteBatch into per-entity writers [DHIS2-21378]

* chore: Migrate ownership from Hibernate to JDBC [DHIS2-21378]

* Code review fixes

(cherry picked from commit 331503a)

* chore: Migrate ownership from Hibernate to JDBC [DHIS2-21378] (#24239)

* refactor: Split EntityWriteBatch into per-entity writers [DHIS2-21378]

* chore: Migrate ownership from Hibernate to JDBC [DHIS2-21378]

(cherry picked from commit 7ba2431)

* perf: Batch existing-TEAV lookup in tracker import [DHIS2-21378] (#24244)

* refactor: Split EntityWriteBatch into per-entity writers [DHIS2-21378]

* chore: Migrate ownership from Hibernate to JDBC [DHIS2-21378]

* Code review fixes

* perf: Batch existing-TEAV lookup in tracker import [DHIS2-21378

* Code review fixes

* Code review fixes

(cherry picked from commit 2972a82)

* perf: Constant-text unnest INSERTs in tracker import [DHIS2-21378] (#24250)

* refactor: Split EntityWriteBatch into per-entity writers [DHIS2-21378]

* chore: Migrate ownership from Hibernate to JDBC [DHIS2-21378]

* Code review fixes

* perf: Batch existing-TEAV lookup in tracker import [DHIS2-21378

* perf: Constant-text unnest INSERTs in tracker import [DHIS2-21378]

* perf: Cache UserInfoSnapshot JSON per batch in tracker import [DHIS2-21378]

* Revert "perf: Cache UserInfoSnapshot JSON per batch in tracker import [DHIS2-21378]"

This reverts commit 7084379.

* Add tests to cover fields import

(cherry picked from commit 7305f9f)

* perf: Cache UserInfoSnapshot JSON per batch in tracker import [DHIS2-21378] (#24252)

* refactor: Split EntityWriteBatch into per-entity writers [DHIS2-21378]

* chore: Migrate ownership from Hibernate to JDBC [DHIS2-21378]

* Code review fixes

* perf: Batch existing-TEAV lookup in tracker import [DHIS2-21378

* perf: Constant-text unnest INSERTs in tracker import [DHIS2-21378]

* perf: Cache UserInfoSnapshot JSON per batch in tracker import [DHIS2-21378]

* Fix duration based Tracker perf tests

* Fix formatting

(cherry picked from commit a03204c)

* fix: Preserve storedby on tracker JDBC writers for 2.43 [DHIS2-21378]

Backport adaptation. On master, DHIS2-21537 removed storedBy from the
TrackedEntity/Enrollment/Event write path (and renamed TEAV storedBy to
updatedBy). 2.43 deliberately keeps storedBy, so the JDBC writers backported
from master must persist the storedby column that 2.43's tables still have.

TrackerObjectsMapper still sets storedBy on the new-entity branch for TE,
enrollment and events, so storedby is insert-only here (mirrors createdby):
added the storedby column + bind to the INSERT of TrackedEntityWriter,
EnrollmentWriter, SingleEventWriter and TrackerEventWriter. TeavWriter was
already adapted (storedby column + getStoredBy) when the earlier commits were
cherry-picked. Keeps the 2.43-only shouldSetStoredByToAuthenticatedUser* tests
green.

* Fix note persister

* Fix formatting
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants