Skip to content

Commit 83b60f1

Browse files
committed
fix: stop scheduler native writes from wiping the whole second level cache
Subset of #24803 by @teleivo, included so this branch is testable standalone; drop this commit once #24803 merges. A native executeUpdate without synchronized query spaces gives Hibernate no way to know which cached entities are affected, so it invalidates EVERY second level cache region. The scheduler writes every ~20 seconds, so all caches were emptied at that cadence, and each wipe triggered a putFromLoad re-population storm under the region write lock. AI Assisted
1 parent 7c6603b commit 83b60f1

1 file changed

Lines changed: 36 additions & 11 deletions

File tree

dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/HibernateJobConfigurationStore.java

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,10 @@ public boolean tryExecuteNow(@Nonnull UID jobId) {
385385
and (schedulingtype != 'ONCE_ASAP' or lastfinished is null)
386386
""";
387387
return runWriteInStatelessSession(
388-
q -> q.createNativeQuery(sql).setParameter("id", jobId.getValue()).executeUpdate())
388+
q ->
389+
synchronizedNativeQuery(q, sql)
390+
.setParameter("id", jobId.getValue())
391+
.executeUpdate())
389392
> 0;
390393
}
391394

@@ -413,7 +416,10 @@ and not exists (
413416
)
414417
""";
415418
return runWriteInStatelessSession(
416-
q -> q.createNativeQuery(sql).setParameter("id", jobId.getValue()).executeUpdate())
419+
q ->
420+
synchronizedNativeQuery(q, sql)
421+
.setParameter("id", jobId.getValue())
422+
.executeUpdate())
417423
> 0;
418424
}
419425

@@ -444,7 +450,10 @@ public boolean tryCancel(@Nonnull UID jobId) {
444450
)
445451
""";
446452
return runWriteInStatelessSession(
447-
q -> q.createNativeQuery(sql).setParameter("id", jobId.getValue()).executeUpdate())
453+
q ->
454+
synchronizedNativeQuery(q, sql)
455+
.setParameter("id", jobId.getValue())
456+
.executeUpdate())
448457
> 0;
449458
}
450459

@@ -475,7 +484,7 @@ public boolean tryFinish(@Nonnull UID jobId, JobStatus status) {
475484
""";
476485
return runWriteInStatelessSession(
477486
q ->
478-
q.createNativeQuery(sql)
487+
synchronizedNativeQuery(q, sql)
479488
.setParameter("id", jobId.getValue())
480489
.setParameter("status", status.name())
481490
.executeUpdate())
@@ -503,7 +512,7 @@ public boolean trySkip(@Nonnull String queue) {
503512
or lastexecuted < (select lastexecuted from jobconfiguration where queuename = :queue and queueposition = 0 limit 1))
504513
""";
505514
return runWriteInStatelessSession(
506-
q -> q.createNativeQuery(sql).setParameter("queue", queue).executeUpdate())
515+
q -> synchronizedNativeQuery(q, sql).setParameter("queue", queue).executeUpdate())
507516
> 0;
508517
}
509518

@@ -521,7 +530,7 @@ public void updateProgress(
521530
""";
522531
runWriteInStatelessSession(
523532
q ->
524-
q.createNativeQuery(sql)
533+
synchronizedNativeQuery(q, sql)
525534
.setParameter("id", jobId.getValue())
526535
.setParameter("json", progressJson)
527536
.setParameter("errors", errorCodes)
@@ -539,7 +548,7 @@ public int updateDisabledJobs() {
539548
where jobstatus = 'SCHEDULED'
540549
and enabled = false
541550
""";
542-
return runWriteInStatelessSession(q -> q.createNativeQuery(sql).executeUpdate());
551+
return runWriteInStatelessSession(q -> synchronizedNativeQuery(q, sql).executeUpdate());
543552
}
544553

545554
@Override
@@ -557,7 +566,7 @@ and now() > lastfinished + :ttl * interval '1 minute'
557566
int deletedCount =
558567
runWriteInStatelessSession(
559568
q ->
560-
q.createNativeQuery(sql)
569+
synchronizedNativeQuery(q, sql)
561570
.setLockOptions(new LockOptions(LockMode.PESSIMISTIC_WRITE).setTimeOut(2000))
562571
.setParameter("ttl", max(1, ttlMinutes))
563572
.executeUpdate());
@@ -574,7 +583,7 @@ and uid not in (select uid from jobconfiguration where schedulingtype = 'ONCE_AS
574583
""";
575584
runWriteInStatelessSession(
576585
q ->
577-
q.createNativeQuery(sql2)
586+
synchronizedNativeQuery(q, sql2)
578587
.setLockOptions(new LockOptions(LockMode.PESSIMISTIC_WRITE).setTimeOut(2000))
579588
.executeUpdate());
580589
return deletedCount;
@@ -606,7 +615,7 @@ and now() > lastalive + :timeout * interval '1 minute'
606615
""";
607616
return runWriteInStatelessSession(
608617
q ->
609-
q.createNativeQuery(sql)
618+
synchronizedNativeQuery(q, sql)
610619
.setParameter("timeout", max(1, timeoutMinutes))
611620
.executeUpdate());
612621
}
@@ -637,10 +646,26 @@ public boolean tryRevertNow(@Nonnull UID jobId) {
637646
and now() > jobconfiguration.lastalive + interval '1 minute'
638647
""";
639648
return runWriteInStatelessSession(
640-
q -> q.createNativeQuery(sql).setParameter("id", jobId.getValue()).executeUpdate())
649+
q ->
650+
synchronizedNativeQuery(q, sql)
651+
.setParameter("id", jobId.getValue())
652+
.executeUpdate())
641653
> 0;
642654
}
643655

656+
/**
657+
* Creates a native query that declares the table it touches. Without a synchronized query space,
658+
* Hibernate cannot know which cached entities a native {@code executeUpdate} affects and
659+
* conservatively invalidates EVERY second level cache region (a wholesale cache wipe). The
660+
* scheduler writes through this store every ~20 seconds, which without this declaration empties
661+
* all caches at that cadence (l2-cache-truth Phase 4 finding, 2026-08-07).
662+
*/
663+
private static NativeQuery<?> synchronizedNativeQuery(StatelessSession session, String sql) {
664+
NativeQuery<?> query = session.createNativeQuery(sql);
665+
query.addSynchronizedQuerySpace("jobconfiguration");
666+
return query;
667+
}
668+
644669
@SuppressWarnings("unchecked")
645670
private static <T> T getSingleResultOrNull(NativeQuery<?> query) {
646671
return (T) query.getResultStream().findFirst().orElse(null);

0 commit comments

Comments
 (0)