fix(push-publish): stop letting a stale index document block unique-field content (#36898) - #37055
Merged
fabrizzio-dotCMS merged 1 commit intoAug 13, 2026
Conversation
…ield content (#36898) When the receiver's search index holds a document whose contentlet is gone from the database -- a "ghost" left behind by an index/database desynchronization -- findUniqueContentMatch() threw a DotDataException and killed the bundle. Since nothing ever cleaned the ghost up, every retry failed the same way, and the only way out was toggling the unique flag off, force-pushing, and toggling it back on. Three changes: - findUniqueContentMatch() now walks every match instead of only the first, and returns the first one that actually resolves from the database. Matches that do not resolve are logged at WARN with the inode, identifier and Lucene query, then skipped. When they all turn out to be ghosts the incoming content is returned untouched, so the bundle imports as new content and unblocks itself. - The Lucene query is restricted to +working:true, which keeps stale live documents from older versions out of the match in the first place. - destroyContentlets() removes the contentlets from the index explicitly. It used to lean on forceUnpublishArchive(), which only covers live content, so destroying working-only content left its document behind -- one of the ways a ghost is born. Removal is deduplicated per identifier/language/variant, which is the granularity of the index document ID. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6 tasks
Contributor
|
Claude finished @fabrizzio-dotCMS's task in 2m 45s —— View job Code Review — Push-publish ghost unique-field fixI read all three changed files and traced the surrounding logic ( Verification notes (why the fix holds up)
New Issues
Both changes are self-healing/idempotent and the primary customer-facing path (all-ghosts skip) is tested and verified correct. No blocking issues. |
freddyDOTCMS
approved these changes
Aug 13, 2026
fabrizzio-dotCMS
deleted the
issue-36898-push-publish-ghost-unique-field
branch
August 13, 2026 23:38
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposed Changes
Fixes #36898
Push-publish was permanently blocked for unique-field content whenever the receiver's search index held a ghost: a document whose contentlet no longer exists in the
contentlettable.findUniqueContentMatch()matched the ghost in the index, failed to resolve it via API, and threw aDotDataExceptionthat aborted the whole bundle. Because nothing ever cleaned the ghost up, every retry failed identically — the only workaround was toggling the unique flag off, force-pushing, and toggling it back on (T3 intervention).This has been confirmed in a customer Cloud environment and is reported across multiple customers.
1. Self-healing on ghost matches —
ContentHandler.javafindUniqueContentMatch()no longer throws. A newresolveUniqueContentMatch()helper walks every result the Lucene query returned (previously onlycontentlets.get(0)was ever considered) and returns the first one that actually resolves from the database. Any match that does not resolve is logged atWARNwith the ghost inode, identifier, the Lucene query used and the unique fields involved, plus a note that a reindex of the affected content is recommended — then skipped.If every match turns out to be a ghost, the incoming
contentis returned untouched, so the bundle is imported as new content and unblocks itself.2. State filter on the Lucene query —
ContentHandler.javaThe query now appends
+working:true, so stale live documents from older versions never reach the resolution step.+deleted:falsewas deliberately not added: matching archived content is intentional here —handleContent()callsunarchive()on a matched archived contentlet before check-in, so excluding archived documents would be a behavior regression.3. Closing one ghost-creation path —
ESContentletAPIImpl.javadestroyContentlets()relied solely onforceUnpublishArchive()for index removal, and that path is gated oncontentlet.isLive(). Working-only content that was never published had its index document left behind when destroyed — one of the ways a ghost is born. The method now callsindexAPI.removeContentFromIndex()explicitly, deduplicated peridentifier/language/variant, which is the granularity of the index document ID (removing once per version would be pure repetition).This mirrors what the sibling delete path already does.
Checklist
Additional Info
Why the ES query change is safe for the ghost path. Fix 2 alone does not close the issue: a ghost whose working document is the stale one still matches. That is precisely why Fix 1 is the required part and Fix 2 is defense in depth.
Existing blocked content unblocks with no reindex and no flag toggling — the very next push-publish attempt succeeds and leaves a
WARNtrail pointing at the desynchronization.Screenshots
N/A — backend only.
This PR fixes: #36898