bug(content-type): changing a content type's publish/expire date field recalculates identifiers with the OLD mapping #38031
Description
Problem Statement
When a content type's publish date or expire date field mapping changes, ContentTypeAPIImpl.transactionalSave fires IdentifierDateJob to recalculate SysPublishDate / SysExpireDate on the identifiers of every existing contentlet of that type. It passes the type as it was before the save:
// ContentTypeAPIImpl.java:1179-1183
final ContentType oldOldType = oldType; // read from the DB before contentTypeFactory.save(...)
if (fireUpdateIdentifiers(oldType.expireDateVar(), contentTypeToSave.expireDateVar())
|| fireUpdateIdentifiers(oldType.publishDateVar(), contentTypeToSave.publishDateVar())) {
IdentifierDateJob.triggerJobImmediately(oldOldType, user);
}IdentifierDateJob.updateIdentifierDates reads the date fields from that type (IdentifierDateJob.java:164-174, type.publishDateVar() / type.expireDateVar(), commented as "the new … Date Var"). So the recalculation applies the previous mapping, not the one just saved:
- Field assigned where there was none → old
expireDateVar()isnull→ the job writesSysExpireDate = nullon every identifier of the type. Existing content never auto-unpublishes (same for publish date / auto-publish). - Field switched A → B → identifiers keep the dates of field A.
- Field removed → identifiers keep the stale dates and keep auto-publishing / unpublishing on a field that is no longer the date field.
Newly saved content is unaffected: a normal save writes the identifier dates directly (ESContentletAPIImpl.java:6687-6699). The defect hits content that existed before the mapping change. Engine-independent (ES and OS alike). The old type has been passed since at least 83ed01e (2019).
Why tests don't catch it: PublisherTest.autoUnpublishContent saves the type and then calls IdentifierDateJob.triggerJobImmediately(contentType, …) itself with the new type (PublisherTest.java:484, added in 6f0b8cf as a flakiness fix). The test passes on that manual run and never exercises the automatic trigger. The two jobs also race: if the automatic one (old type, writes null) finishes last, it overwrites the correct value — a plausible contributor to the flake tracked in #38006 (not verified).
Steps to Reproduce
- Create a content type with a Date/Time field
expiresand no expire date field set. - Create and publish a contentlet with
expires= yesterday. - Edit the content type and set
expiresas its Expire Date Field; save. - Wait for
IdentifierDateJobto run, then check the identifier:SELECT syspublish_date, sysexpire_date FROM identifier WHERE id = '<identifier>';
Expected: sysexpire_date = the contentlet's expires value, and the content is unpublished by the next PublishDateUpdater run.
Actual: sysexpire_date is NULL; the content stays live.
Acceptance Criteria
- Changing a content type's publish/expire date field recalculates existing identifiers with the newly saved mapping (pass
contentTypeToSave, notoldOldType). - Assigning, switching and removing the date field each leave identifier dates matching the new mapping.
-
PublisherTest.autoUnpublishContentrelies on the automatic trigger — the manualtriggerJobImmediatelycall is removed so the real path is tested.
dotCMS Version
main (3a6eaa8042); present since at least 2019.
Severity
Medium - Some functionality impacted
Links
- bug(index): IdentifierDateJob misses just-saved content due to index read-after-write visibility race #38006 —
IdentifierDateJobread-after-write visibility race (sibling) - bug(index): product fixes & findings from phased (ES→OS) integration runs #36501 — phased integration runs where this was found (D12)
Metadata
Metadata
Assignees
Type
Projects
- StatusShow more project fieldsNew
Activity