Bug: PublisherQueueJob never reaches FAILED_TO_PUBLISH when receiver audit poll fails (token/network)
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Problem Statement
When a push-publish bundle ends PushPublisher.process() in FAILED_TO_SEND_TO_ALL_GROUPS or FAILED_TO_SEND_TO_SOME_GROUPS because of a permanent receiver-side failure (invalid PP token returning HTTP 401, or an unreachable host), the bundle never transitions to the terminal FAILED_TO_PUBLISH status. The audit row stays in a non-terminal "pending" state indefinitely, and the PublisherQueueJob Quartz tick logs a stack trace every minute.
Mechanism (file: dotCMS/src/main/java/com/dotcms/publisher/business/PublisherQueueJob.java):
- After the initial failed send,
PushPublisher.process()marks the bundle asFAILED_TO_SEND_TO_ALL_GROUPS(or_SOME_GROUPS). Both codes are in the whitelist returned byPublishAuditAPIImpl.getPendingPublishAuditStatus()(PublishAuditAPIImpl.java:531-547), so the bundle is picked up byPublisherQueueJob.updateAuditStatuson the next tick (PublisherQueueJob.java:238-262). updateAuditStatuscallscollectEndpointInfoFromRemote(line 249-250). With a permanently-bad token,getRemoteHistoryFromEndpoint(line 650-669) throwsDotDataException— either because Jersey's POST fails or becauseJsonUtil.getObjectFromJsoncannot parse the non-JSON 401 body.- The outer
sendBundle(line 334-348) declaresthrows DotDataExceptionand does not catch the call togetRemoteHistoryFromEndpoint. The innersendBundle's try/catch (line 350-368) sits one frame too deep — it only protects per-row updates after the remote history has been fetched successfully. updateAuditStatushas onlytry/finallyper bundle (line 244-260), nocatch. The exception propagates out of theforloop, breaking iteration over the remaining pending bundles.execute()catchesThrowableat the outermost level (line 222-225) and logs the error.- Because
updateBundleStatus(line 387-507) is never reached for the failing bundle, neithernumTriesnor theMAX_NUM_TRIESsafety net at line 408 ever trigger. The bundle stays atFAILED_TO_SEND_TO_ALL_GROUPSforever and the poller fails on it every minute.
Additional collateral damage:
- One poisoned bundle starves the entire
updateAuditStatuspass: every subsequent bundle inpendingBundleAuditsis skipped that tick because the exception breaks theforloop. - For the specific 401 case,
PushPublisher.updatingPublishingDetailStatusalso callsdeleteElementsFromPublishQueueTable(line 410), so the bundle isn't re-processed bySQLGETBUNDLESTOPROCESS— no furthernumTriesincrements fromPushPublisher.process()either.
Impact:
- Bundles that fail with a permanent auth or connection error never reach the terminal
FAILED_TO_PUBLISHstate. - Their
publishing_queue_auditrows never get cleaned up (updateBundleStatusline 418-419 /deleteElementsFromPublishQueueTableare never invoked). - Every
PublisherQueueJobtick prints a stack trace and stops processing other pending bundles after the bad one.
Related to #34356 (push-publish failure events enhancement) — distinct correctness bug uncovered while reviewing that flow.
Steps to Reproduce
- Configure a sending environment with at least one Push-Publishing endpoint whose Auth Token is invalid (or whose receiver is unreachable / returns 401 for
/api/auditPublishing/getAll). - Push any bundle (e.g. a single page) to that environment from the Bundle Uploader or the Push Publishing UI.
- Observe
PushPublisher.process()set the audit row toFAILED_TO_SEND_TO_ALL_GROUPSand fireAllPushPublishEndpointsFailureEvent. - Wait ≥ 1 minute and tail the dotCMS logs: each
PublisherQueueJob.executetick logs aDotDataExceptionoriginating fromgetRemoteHistoryFromEndpoint, and the bundle's status inpublishing_queue_auditnever advances pastFAILED_TO_SEND_TO_ALL_GROUPSeven afternumTrieswould otherwise have exceededMAX_NUM_TRIES(default 3 fromPUBLISHER_QUEUE_MAX_TRIES). - Confirm via DB:
SELECT bundle_id, status, status_pojo FROM publishing_queue_audit WHERE bundle_id = '<id>'— status stays at4(FAILED_TO_SEND_TO_ALL_GROUPS) indefinitely.
Acceptance Criteria
- After
MAX_NUM_TRIESfailed receiver-poll attempts, the bundle transitions to terminalFAILED_TO_PUBLISH(status code 8) and the queue rows are cleaned up — same behaviourupdateBundleStatusline 408-419 already implements for other failure paths. - A single failing bundle does not abort polling for the remaining pending bundles in the same
execute()tick. - On an exception from
getRemoteHistoryFromEndpoint, theendpointTrackingMapfor the bundle records a synthetic per-endpoint failure (statusFAILED_TO_PUBLISH) — analogous to the existing innersendBundlecatch at lines 362-366 — soupdateBundleStatuscan run, incrementnumTries, and reach theMAX_NUM_TRIESsafety net. - Unit/integration test reproducing the loop: a bundle marked
FAILED_TO_SEND_TO_ALL_GROUPSwithnumTries = 1reachesFAILED_TO_PUBLISHafterMAX_NUM_TRIESticks when the receiver mock throws on/api/auditPublishing/getAll. - No regression in the happy path: bundles that poll successfully still transition through
RECEIVED_BUNDLE/PUBLISHING_BUNDLE/SUCCESSas before.
Suggested fix (sketch — open to discussion):
- Wrap the per-bundle body of
updateAuditStatus'sforloop with atry/catchso one bundle's failure can't break the others. - Hoist the inner
sendBundletry/catch one frame up — around the call togetRemoteHistoryFromEndpointinside the outersendBundle— and on exception synthesize the same"failed-remote-group-…"EndpointDetailwithStatus.FAILED_TO_PUBLISHthat the inner catch already produces at lines 362-366. That wayendpointTrackingMapcaptures the failure,updateBundleStatusruns,numTriesincrements, and afterMAX_NUM_TRIESthe bundle correctly transitions toFAILED_TO_PUBLISH.
dotCMS Version
latest Evergreen 26.05.11-01
Severity
Medium - Some functionality impacted
Links
NA
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with dotCMS/src/main/java/com/dotcms/publisher/business/PublisherQueueJob.java, especially updateAuditStatus, sendBundle, and updateBundleStatus, then trace getRemoteHistoryFromEndpoint in PublishAuditAPIImpl and the related PushPublisher flow. Add a regression test using a receiver mock that fails during audit polling, and verify retry exhaustion reaches FAILED_TO_PUBLISH, cleans queue rows, continues processing later bundles, and preserves successful polling behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 52/100