Skip to content

fix: invalidate template cache and use live template when sending notifications [DHIS2-21836] - #24732

Open
enricocolasante wants to merge 1 commit into
masterfrom
DHIS2-21836
Open

fix: invalidate template cache and use live template when sending notifications [DHIS2-21836]#24732
enricocolasante wants to merge 1 commit into
masterfrom
DHIS2-21836

Conversation

@enricocolasante

@enricocolasante enricocolasante commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes DHIS2-21836: after editing a program notification template to deselect the email delivery channel, users still received emails (even with no channel or only SMS selected), and clearing the cache did not help. The template was being read from an in-memory cache that the metadata edit path never invalidated, and scheduled program-rule notifications were reading a frozen template snapshot instead of the current template. Both are now fixed so channel edits take effect immediately.

Changes

  • Cache invalidation on the real edit path (ProgramNotificationTemplateObjectBundleHook.java): template edits go through the metadata/ObjectBundle pipeline, which bypasses ProgramNotificationTemplateService.save/update/delete — the only place the getByUidCached cache was invalidated. The hook now evicts the cache in postCreate, postUpdate, and preDelete, so a deselected channel is honored on the next notification instead of serving the stale pre-edit template.
  • Service cache API (ProgramNotificationTemplateService.java, DefaultProgramNotificationTemplateService.java): adds invalidateCache(String uid) to the interface and implementation, and routes the existing save/update/delete invalidations through it.
  • Prefer live template over snapshot (DefaultProgramNotificationService.java): getApplicableTemplate now uses the live database template and only falls back to the frozen JSONB snapshot when the template has been deleted. This makes edits take effect on already-scheduled program-rule notifications while preserving the snapshot's purpose of surviving template deletion. The misleading log.warn now fires only when no template can be resolved at all.
  • Snapshot mapping fix (NotificationTemplateMapper.java): corrects setSendRepeatable(t.isSendRepeatable()), which read from the snapshot being built instead of the source template, so sendRepeatable is now captured.
  • Tests: ProgramNotificationTemplateObjectBundleHookTest verifies the cache is invalidated on create/update/delete; ProgramNotificationServiceTest adds cases proving the live template wins over a stale snapshot and that the snapshot is still used when the template was deleted.

@sonarqubecloud

sonarqubecloud Bot commented Aug 4, 2026

Copy link
Copy Markdown

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 29.62963% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 47.58%. Comparing base (e4a9688) to head (5cda554).
⚠️ Report is 75 commits behind head on master.

Files with missing lines Patch % Lines
...otification/DefaultProgramNotificationService.java 0.00% 17 Missing ⚠️
...ram/DefaultProgramNotificationTemplateService.java 60.00% 2 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             master   #24732       +/-   ##
=============================================
+ Coverage     35.47%   47.58%   +12.10%     
+ Complexity      533      388      -145     
=============================================
  Files          3713     3705        -8     
  Lines        143929   144159      +230     
  Branches      16776    16823       +47     
=============================================
+ Hits          51064    68595    +17531     
+ Misses        88660    68988    -19672     
- Partials       4205     6576     +2371     
Flag Coverage Δ
integration 47.58% <29.62%> (?)
unit ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...ification/template/NotificationTemplateMapper.java 49.43% <100.00%> (-23.60%) ⬇️
...s/ProgramNotificationTemplateObjectBundleHook.java 70.00% <100.00%> (+70.00%) ⬆️
...ram/DefaultProgramNotificationTemplateService.java 64.51% <60.00%> (+64.51%) ⬆️
...otification/DefaultProgramNotificationService.java 21.29% <0.00%> (-42.00%) ⬇️

... and 2629 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 5115a2c...5cda554. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@muilpp
muilpp requested a review from a team August 12, 2026 08:08
// The metadata import pipeline persists directly through the store and bypasses
// ProgramNotificationTemplateService, so the getByUidCached cache would otherwise keep serving
// the pre-edit template (e.g. still delivering email after the channel was deselected).
programNotificationTemplateService.invalidateCache(template.getUid());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invalidateCache is called from postCreate/postUpdate/preDelete, all of which run inside ObjectBundleService.commit()'s @Transactional boundary before commit. I think this is the same issue as the evict-at-flush in #24810 (review). This will remove a cached entry causing a concurrent reader to miss and fetch the template. PGs default isolation read-committed means the reader could still read a stale template (like the one that was just evicted). Only the TTL or another edit to the template would then evict that stale template.

The difference to the linked PR is that Hibernate also evicts after commit, which clears the stale entry unless the reader's put lands after it (see PR comment for the race it faces).

We could try to use TransactionSynchronization and only evict in afterCommit. That fixes the above issue. A race is still possible: cold cache, and the reader's SELECT must precede commit-visibility while its put lands after afterCommit. The difference is that evicting inside the transaction creates the miss, so any reader during an edit gets a stale entry, while with afterCommit the reader has to arrive already-missing.

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.

4 participants