Spec: silent index delete loss (#37276) - #37297
Conversation
|
Claude finished @fabrizzio-dotCMS's task in 4m 38s —— View job Code review — PR #37297 (silent index delete loss)I reviewed the implementation diff ( New Issues
Notes (non-blocking)
Resolved (reviewer-raised gaps)
Tests cover the partial-failure verdict (ES/OS unit tests), the batch-key resolution, and the destroy/skip/phase integration paths — coverage looks appropriate for the behavior added.
|
AC-001 overclaimed the retry guarantee: markAsFailed parks a row above ERROR priority after REINDEX_MAX_FAILURE_ATTEMPTS and the drain query selects priority <= 400, so an exhausted removal is never retried again. Reword to at-least-once-up-to-N and add AC-007 requiring the exhausted entry stay discoverable in the journal with its cause. Add the matching non-goal. Bring the reindex batch-collision fix into scope with AC-008 and a blast-radius note, from review feedback on PR #37297 — it was driving a blocking phase in the task list while tracing to nothing in the approved spec. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Three gaps I found:
|
…ed (#37276) Content destruction deleted the database rows transactionally, then handed the index removal to an in-memory post-commit listener that recorded nothing durable. Any loss of that task — the JVM stopping between commit and execution, the shared pool rejecting it, or a bulk returning per-item failures that were logged and treated as success — orphaned the index document permanently, with no error and nothing to retry. Adds are already backed by dist_reindex_journal: the intent is written inside the transaction and ReindexThread retries it until acknowledged. The equivalent delete machinery existed end to end — ReindexAction.DELETE, addIdentifierDelete, the read-back flag, and both the ES and OS bulk consumers — with zero production callers. This wires it up. - destroyContentlets and deleteAllVersionsandBackup enqueue a DELETE entry in the same transaction that deletes the rows. delete(List, User, boolean, boolean) is deliberately excluded: it can delete a subset of an identifier's versions, and a journal entry is identifier-wide. - ReindexQueueFactory.findContentToReindex resolves a REINDEX/DELETE collision for one identifier by row id instead of poll order. The batch is keyed by identifier alone while ReindexEntry equality includes the delete flag, so the pair was not deduplicated but did overwrite each other. Unreachable until deletes are enqueued; routine afterwards (content saved, then destroyed). - ContentletIndexOperationsES and ...OS raise on a partially failed bulk instead of logging and returning. The escalation is in the providers, not the router, so the dual-write shadow leg stays isolated per ADR-0009. - A removal skipped because the PRIMARY provider's index pointers will not load no longer looks like a completed removal. Shadow providers keep warn-and-continue. - Bulk failure messages no longer say "Error reindexing" for removals — that wording is why searching production logs for failed deletes came back empty. - Corrects IndexPolicyProvider javadoc claiming a WAIT_FOR default; it is DEFER. Spec approved in #37297. Includes the data model, the putToIndex contract change and the release note. Verification: 278 unit tests green across the index and reindex packages, including 12 new ones. The 8 new integration tests are registered in MainSuite1b/2b but have NOT been run locally — the Docker daemon died mid-verification. They must go green in CI before merge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…37276) Integration testing surfaced a second way the removal path goes silent, adjacent to the one this PR already fixed. loadProviderIndicesQuietly returns null only when loading the pointers *throws*. When the pointers load but hold no active index, it returns a non-null ProviderIndices with null slots: no delete operations are added, putToIndex early-returns on the empty batch, and the caller sees success — exactly as invisibly as the throwing case. Count the delete operations the primary contributed instead of testing for a null ProviderIndices. Zero operations means the removal did not happen, whichever way the primary came up empty. Also fixes the test's failure injection: emptying the indicies table is not enough on its own, because IndiciesAPI reads through IndiciesCache and the stale pointers stayed visible, so the removal proceeded normally and the test proved nothing. Flush the cache to force it, and restore via IndiciesAPI#point rather than leaving the table empty for the rest of the suite. Verified locally: all 9 integration tests across the four classes pass, run together in one JVM; 278 unit tests still green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4326b50
Four gaps from review of #37297 and #37320, all of which were decisions taken in code without being written down — the same failure mode the reviewer flagged. In scope, now stated: - deleteAllVersionsandBackup, the second site that defers an index removal. Its version list comes from findAllVersions(identifier), so an identifier-wide journal entry is correct there. - The OpenSearch write leg once OpenSearch serves reads. From Phase 2 onwards reads come from OS while writes stay ES-primary / OS-shadow, so a removal lost on the OS leg orphaned a document in the index being queried — this defect, in the phase the migration spends the longest in. Non-goals, now stated: - ContentletAPI#delete(List, User, boolean, boolean), the third site with this shape. It can delete a subset of an identifier's versions, and a journal entry is identifier-wide. - Surfacing drift in the Maintenance portlet (F4 in the issue). AC-007 already makes the residue enumerable; the operator view is separate work. This had been dropped silently, which was the fair complaint. - Phase 1 shadow durability. It stays fire-and-forget per ADR-0009; only the Phase 2 assumption that a shadow is not read from is corrected. New acceptance criteria: - AC-009: enumerating putToIndex callers is a prerequisite for AC-003, not a follow-up. It was previously only prose in Regression Risk, which is exactly how it could have slipped through planning. - AC-010: where OS serves reads, a failed OS write reaches the caller and marks the journal entry for retry; where nothing reads OS, it stays swallowed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All four gaps from review are closed — three in the spec (945d27e), one in code plus spec. @ihoffmann-dot, your three:
The claude[bot] review also found something sharper than a spec gap, and it turned out to be real: from Phase 2 onwards Fixed in #37333 by scoping the shadow treatment to who serves reads rather than to dual-write, with AC-010 added here. ADR-0009's intent is preserved — Phase 1 still swallows, and there is a test pinning that so the fix cannot over-reach. |
|
Closing out the two 🟡 findings from the automated review — both were real and both are fixed. 1. Blank exception message in 2. Phase 2 read/shadow gap. Confirmed, and it was the sharpest thing in either review. You suggested it might be intentionally out of scope under ADR-0009 and that a non-goal note would close the gap. I went the other way and fixed it, because the ADR's rationale is "a store nobody reads from must not break a user operation" — and in Phase 2 that premise no longer holds. Scoping the shadow treatment by Worth noting the async path mattered more than the sync one you pointed at: Fix in #37333 with |
Defect spec for #37276. Implementation follows in PR 2.
This PR fixes: #37276