Skip to content

feat: Digest sender job, safety cleanup and email assembly - EXO-89539_EXO-89982_EXO-89486_EXO-89487_EXO-90020 - eXIP7.3.0.22 - #775

Merged
azayati merged 8 commits into
feature/experiencefrom
exip-7.3.0.22-us08-digest-sender
Sep 4, 2026
Merged

azayati merged 8 commits into
feature/experiencefrom
exip-7.3.0.22-us08-digest-sender

Conversation

@azayati

@azayati azayati commented Sep 3, 2026

Copy link
Copy Markdown
Member

eXIP 7.3.0.22 Digest Mail Notifications — US08 "Digest sending schedule and coverage" (EXO-89539), with the email assembly shared by US06 (daily) and US07 (weekly). Spec: Tribe note 50469, §4 Scheduling / Assembly / Data.

What it does

  • One hourly job (DigestSenderJob, cron exo.notification.digest.job.expression, default 0 0 * * * ?), glue only, run on its own thread, one run at a time. It never stops: the admin switch OFF stops only the email production, the claims, watermarks, deletions and cleanup go on (settled decision, spec §4 Admin switch).
  • Run (DigestSender): safety cleanup (orphans + retention exo.notification.digest.retention.days, default 8) → indexed candidates → exact "due now?" in the user's timezone (DigestDueCalculator: daily.hour 18, weekly.day FRIDAY; at most one per local day / week, catch-up after a missed run, first digest the day after opt-in, no double email around a timezone change) → claim by guarded watermark UPDATE (the cross-node protection) → email → mail queue → deletion of the items covered by every enabled frequency. The two frequencies of one user are served in sequence by one task. A failure after the claim gives the occurrence back.
  • Email (DigestMailBuilder + DigestMail.gtmpl): categories of the frequency only (second half of the double check), display order, cap daily.cap 5 / weekly.cap 10 with "+ N more →", subject / greeting / intro / footer links per the board texts, everything escaped, translations text-only with {i} placeholders (platform convention). Zero lines ⇒ no email, the watermark still moves.
  • SPI for the addons: DigestLinePlugin (Kernel plugin declared in XML next to the category, addLineProvider on DigestCategoryRegistry) builds one DigestLine (label key + args + link) from the stored ids; a vanished object gives no line and is not counted. The five addon PRs (social, content, task, agenda, processes) follow.
  • Capture side: ITEM_DATE is now the capture moment; PARAMS shrinks longest-value-first instead of being dropped.

Tests

93 digest tests green in commons-component-common, including DigestUserDAOTest and DigestItemDAOTest (the hand-written JPQL run on HSQLDB through the kernel persistence unit), DigestDueCalculatorTest (15 calendar scenarios incl. the spec's timezone edge cases), DigestMailBuilderTest (renders the real template), DigestSenderTest, DigestSenderJobTest.

Self-review close-out (3 independent rounds + refute pass)

Round Findings Status
1 🔴 daily sent at midnight after an opt-in before 18:00 · 🟠 release equality vs MySQL TIMESTAMP rounding · 🟠 daily/weekly race on one user · 🟠 unreadable template cached as empty · 🟠 PARAMS dropped above 2000 · 🟡 MessageFormat vs {i} · 🟡 JPQL not engine-run · 🟡 spec drifts · 🟡 duplicate label resolver · 🟢 nits all fixed (the red one mutation-verified) except 2 accepted: spec drifts → resync list; label resolver duplicate → social PR switches to the commons bean
2 🟠 late asynchronous capture row deleted unread · 🟡 pin not discriminating · 🟡 run blocking the shared scheduler thread · 🟡 null line argument · 🟢 placeholder re-expansion · 🟢 log noise all fixed
3 🟡 job glue untested · 🟢 huge placeholder index · 🟢 flag stuck at shutdown · 🟢 ITEM_DATE wording all fixed; nothing open

Verified conform by the reviewers: job = glue calling the Service layer with a property cron; Service → Storage → DAO layering, constructor injection; every hand-written JPQL executed by the engine; admin-OFF semantics as settled; double check of categories; caps, counts and never-send-empty; escaping of every value put in the HTML; war: template resolution from commons-extension-webapp; mail queue contract (pluginId "digest", sender/recipient from the existing helpers).

Accepted trade-offs for the approver: no platform "one node only" job guard exists (spec assumption A2 is false) — the watermark claim is the protection; claim (Spring tx) and queueing (Kernel tx) are two transactions, release compensates; derived-name Spring Data queries are validated at the social webapp start, not in this suite.

Spec resync to propose before merge (note 50469 §4): first digest the day after opt-in; ITEM_DATE = capture moment; cleanup = orphans + retention (the category step is redundant by construction); no MailTemplateContentTransformerService pass (links absolute, escaping upstream); line providers as a second Kernel plugin; PARAMS shrinks longest-first; A2 false.

Classification: N1 (core notification path, new DAO surface and plugin SPI in commons, mass email). Human approval required, author ≠ approver.

Knowledge: none — the commons domain doc fragment (digest engine, DigestLinePlugin SPI) is drafted with the last PR of this lot, once the five addon PRs have landed the same SPI.

🤖 Generated with Claude Code

azayati and others added 4 commits September 3, 2026 19:42
One hourly Spring scheduled job (started by the social webapp context) serves
every user whose daily or weekly digest is due in his own timezone: claim by
guarded watermark update, email built from the waiting items through the
addons' line providers, mail queue, deletion of the covered items, safety
cleanup first. The administrator switch gates only the email production: the
job never stops and the watermarks keep moving.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- due rule: a watermark from the occurrence's own day blocks it (no midnight send)
- daily and weekly of one user served in sequence by one task
- unreadable template is an error, never an empty email
- PARAMS shrinks by dropping the longest values, keeps the ids
- {i} plain placeholder replacement like the other notification bundles
- watermarks in whole seconds, release outcome logged
- JPQL statements shared as constants and run on the engine in tests

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- ITEM_DATE is the insert moment: a row written during a run belongs to the next window
- the run leaves the Spring scheduler thread, one run at a time
- DigestLine.of tolerates a null argument
- placeholders replaced in one pass, arguments are never re-expanded
- the per-user grouping pin now counts the tasks
- one stack trace per failure, at debug level

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- job glue tested: skip while running, failure never blocks the next run
- oversized placeholder index is text, rejected submission resets the flag
- DigestItem date documented as the capture moment

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@azayati

azayati commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

Addon PRs implementing the DigestLinePlugin SPI of this PR (merge this one first): Meeds-io/social#6072, Meeds-io/content#911, Meeds-io/task#642, exoplatform/agenda#1107, exoplatform/processes#493. One independent review round was run over the five together with this branch; nothing open.

@azayati azayati changed the title feat: Digest sender job and email assembly - EXO-89539 - eXIP7.3.0.22 feat: Digest sender job, safety cleanup and email assembly - EXO-89539 - EXO-89982 - eXIP7.3.0.22 Sep 4, 2026
@azayati azayati changed the title feat: Digest sender job, safety cleanup and email assembly - EXO-89539 - EXO-89982 - eXIP7.3.0.22 feat: Digest sender job, safety cleanup and email assembly - EXO-89539_EXO-89982_EXO-89486_EXO-89487 - eXIP7.3.0.22 Sep 4, 2026
@azayati azayati changed the title feat: Digest sender job, safety cleanup and email assembly - EXO-89539_EXO-89982_EXO-89486_EXO-89487 - eXIP7.3.0.22 feat: Digest sender job, safety cleanup and email assembly - EXO-89539_EXO-89982 - eXIP7.3.0.22 Sep 4, 2026
@azayati azayati changed the title feat: Digest sender job, safety cleanup and email assembly - EXO-89539_EXO-89982 - eXIP7.3.0.22 feat: Digest sender job, safety cleanup and email assembly - EXO-89539_EXO-89982_EXO-89486_EXO-89487 - eXIP7.3.0.22 Sep 4, 2026
… - eXIP7.3.0.22

A work list row whose settings show no enabled frequency has no owner any
more (the settings vanish with the account): at his next occurrence the job
deletes the row and every waiting item instead of serving a ghost forever.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@azayati

azayati commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

Added (EXO-89982, from Ayoub's test of an account deletion): a user whose digest settings are gone (both frequencies read off while his work list row still exists — the settings are purged with the account) is forgotten by the job at his next occurrence: the NTF_DIGEST_USERS row and every NTF_DIGEST_ITEMS row of the user are deleted, nothing is built. No listener, no separate PR: it is part of the cleanup mechanism of the job. Tests: DigestSenderTest.testUserWhoseSettingsAreGoneIsForgotten, DigestQueriesEngineTest.testForgettingAUserDeletesAllHisItemsOnly.

azayati and others added 3 commits September 4, 2026 12:44
…-89539 - eXIP7.3.0.22

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
….0.22

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Sep 4, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
1 New Critical Issues (required ≤ 0)
D Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@azayati
azayati merged commit 1da28e4 into feature/experience Sep 4, 2026
4 of 5 checks passed
@azayati
azayati deleted the exip-7.3.0.22-us08-digest-sender branch September 4, 2026 15:07
exo-swf pushed a commit that referenced this pull request Sep 4, 2026
…9_EXO-89982_EXO-89486_EXO-89487 - eXIP7.3.0.22 (#775)
@azayati azayati changed the title feat: Digest sender job, safety cleanup and email assembly - EXO-89539_EXO-89982_EXO-89486_EXO-89487 - eXIP7.3.0.22 feat: Digest sender job, safety cleanup and email assembly - EXO-89539_EXO-89982_EXO-89486_EXO-89487_EXO-90020 - eXIP7.3.0.22 Sep 7, 2026
azayati added a commit that referenced this pull request Sep 7, 2026
…- eXIP7.3.0.22 (#779)

eXIP 7.3.0.22 Digest Mail Notifications — US08 (EXO-89539), spec §4
Timezone: "When a timezone is saved, that service broadcasts an event,
and a small digest listener refreshes TIMEZONE in NTF_DIGEST_USERS."
This listener was missing from #775; the functional
tests showed the copy only refreshed on Apply.

## What it adds

- `DigestTimeZoneListener`, a Kernel listener on `social.timeZone.saved`
(event broadcast by social's `UserTimeZoneService`, source = username,
data = zone id), asynchronous and fail-safe: it hands the new zone to
`DigestService.updateTimeZone`, and a failure is logged without
affecting the timezone save. commons doesn't depend on social: the event
name is the contract, like the capture listener.
- `DigestService.updateTimeZone(username, timeZone)` →
`DigestEnrollmentStorage.updateTimeZone`: refreshes the copy of an
enrolled user, writes nothing for a user without digest or an unchanged
zone.
- Declared in `conf/portal/configuration.xml` next to the capture
listener.

Tests: `DigestTimeZoneListenerTest` (event handed over, failure
contained), `DigestEnrollmentStorageTest` (refreshed / ignored without
digest / unchanged zone). Digest tests green.

Note: `DigestEnrollmentStorageTest` is also created by
#778 with other methods; the second one to merge takes a
trivial file merge, I will handle it.

Classification: **N2** (glue listener + one storage method inside the
digest engine).

Knowledge: none.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
azayati added a commit that referenced this pull request Sep 8, 2026
…9_EXO-89982_EXO-89486_EXO-89487 - eXIP7.3.0.22 (#775)
azayati added a commit that referenced this pull request Sep 8, 2026
…- eXIP7.3.0.22 (#779)

eXIP 7.3.0.22 Digest Mail Notifications — US08 (EXO-89539), spec §4
Timezone: "When a timezone is saved, that service broadcasts an event,
and a small digest listener refreshes TIMEZONE in NTF_DIGEST_USERS."
This listener was missing from #775; the functional
tests showed the copy only refreshed on Apply.

## What it adds

- `DigestTimeZoneListener`, a Kernel listener on `social.timeZone.saved`
(event broadcast by social's `UserTimeZoneService`, source = username,
data = zone id), asynchronous and fail-safe: it hands the new zone to
`DigestService.updateTimeZone`, and a failure is logged without
affecting the timezone save. commons doesn't depend on social: the event
name is the contract, like the capture listener.
- `DigestService.updateTimeZone(username, timeZone)` →
`DigestEnrollmentStorage.updateTimeZone`: refreshes the copy of an
enrolled user, writes nothing for a user without digest or an unchanged
zone.
- Declared in `conf/portal/configuration.xml` next to the capture
listener.

Tests: `DigestTimeZoneListenerTest` (event handed over, failure
contained), `DigestEnrollmentStorageTest` (refreshed / ignored without
digest / unchanged zone). Digest tests green.

Note: `DigestEnrollmentStorageTest` is also created by
#778 with other methods; the second one to merge takes a
trivial file merge, I will handle it.

Classification: **N2** (glue listener + one storage method inside the
digest engine).

Knowledge: none.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
exo-swf pushed a commit that referenced this pull request Sep 8, 2026
…9_EXO-89982_EXO-89486_EXO-89487 - eXIP7.3.0.22 (#775)
exo-swf pushed a commit that referenced this pull request Sep 8, 2026
…- eXIP7.3.0.22 (#779)

eXIP 7.3.0.22 Digest Mail Notifications — US08 (EXO-89539), spec §4
Timezone: "When a timezone is saved, that service broadcasts an event,
and a small digest listener refreshes TIMEZONE in NTF_DIGEST_USERS."
This listener was missing from #775; the functional
tests showed the copy only refreshed on Apply.

## What it adds

- `DigestTimeZoneListener`, a Kernel listener on `social.timeZone.saved`
(event broadcast by social's `UserTimeZoneService`, source = username,
data = zone id), asynchronous and fail-safe: it hands the new zone to
`DigestService.updateTimeZone`, and a failure is logged without
affecting the timezone save. commons doesn't depend on social: the event
name is the contract, like the capture listener.
- `DigestService.updateTimeZone(username, timeZone)` →
`DigestEnrollmentStorage.updateTimeZone`: refreshes the copy of an
enrolled user, writes nothing for a user without digest or an unchanged
zone.
- Declared in `conf/portal/configuration.xml` next to the capture
listener.

Tests: `DigestTimeZoneListenerTest` (event handed over, failure
contained), `DigestEnrollmentStorageTest` (refreshed / ignored without
digest / unchanged zone). Digest tests green.

Note: `DigestEnrollmentStorageTest` is also created by
#778 with other methods; the second one to merge takes a
trivial file merge, I will handle it.

Classification: **N2** (glue listener + one storage method inside the
digest engine).

Knowledge: none.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
azayati added a commit that referenced this pull request Sep 9, 2026
…9_EXO-89982_EXO-89486_EXO-89487 - eXIP7.3.0.22 (#775)
exo-swf pushed a commit that referenced this pull request Sep 11, 2026
…9_EXO-89982_EXO-89486_EXO-89487 - eXIP7.3.0.22 (#775)
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.

1 participant