fix(opensearch): surface a shadow store that stopped accepting writes (#36222) - #36903
fix(opensearch): surface a shadow store that stopped accepting writes (#36222)#36903fabrizzio-dotCMS wants to merge 2 commits into
Conversation
…#36222) In dual-write phases an OpenSearch write failure is fire-and-forget, and the reindex listener logged one WARN per failed item with nothing else. When the OS user loses a permission every document of every batch is rejected with the same message, which produced two problems observed while QA-ing #36222 (TC-056): ~900 identical warnings in one minute (a real reindex emits hundreds of thousands, burying every other line), and no signal at all that the shadow store had stopped receiving writes — so advancing the migration phase would silently switch reads to a frozen index. - BulkProcessorListener: aggregate a batch's failures into one line per distinct message with its count, keep the per-item detail at DEBUG, and log a single actionable ERROR when the batch was rejected in full for a systemic cause (rate-limited via REINDEX_SHADOW_FAILURE_ESCALATION_MINUTES, default 10). Partial failures and unclassifiable causes stay silent: those are per-document problems, not migration blockers. The phase is NOT reset here — a bulk rejection can be scoped to one index, so halting stays the operator's call. - OSIndexAPIImpl: classify a bare failure message, not only an exception chain (a rejected bulk item carries text, never a status code), and recognise the security plugin's own wording (security_exception / no permissions for). - IndexConfigHelper: neutral seam so the reindex listener escalates without importing the OpenSearch adapter's classifier. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @fabrizzio-dotCMS's task in 34s —— View job Claude finished @fabrizzio-dotCMS's task in 2m 58s —— View job Code Review — shadow bulk failure escalation
The change is well-scoped and well-tested — aggregation, dominant-cause selection, and the "must not escalate" cases are all covered. Two non-blocking items below. New Issues
Notes (non-blocking)
Everything else — the "does NOT escalate on partial/unclassifiable" policy, per-item detail dropped to DEBUG, the neutral · |
…rEvery (#36222) Replaces the ad-hoc AtomicLong throttle and its config key (REINDEX_SHADOW_FAILURE_ESCALATION_MINUTES) with the existing rate-limited logging mechanism: Logger.warnEvery already keys an ExpiringMap by message, so this adds the missing ERROR-level sibling instead of reinventing it. The throttle identity is now the dominant rejection message, so a change of cause is reported immediately rather than waiting out the interval. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| // line per failed item buried the log — a real reindex emits hundreds of thousands of | ||
| // identical entries, hiding every other line including the actionable one below | ||
| // (observed on issue #36222, TC-056: ~900 identical WARNs in one minute). | ||
| logShadowBatchFailures(results); |
There was a problem hiding this comment.
should we wrap this in a Try.of or try/catch and log if there are any exceptions before rethrowing? I know exceptions can happen in the reactor class which basically take it down without any messages (older issue, probably fixed), just want this to be as robust as possible?
What
In dual-write phases OpenSearch is a shadow store and its write failures are fire-and-forget. The reindex listener logged one WARN per failed item and nothing else, which is right for a single malformed contentlet and wrong for the case QA hit while exercising #36222 (TC-056): the OS user lost a write permission, so every document of every batch was rejected with the same message.
Two consequences, both visible in the QA log:
security_exceptionwarnings in one minute. A real site's reindex emits hundreds of thousands, burying every other entry — including the actionable ones we added in fix(opensearch): degrade OS index-bootstrap failures to ES-only in dual-write phases (#36222) #36783.#36783covered the same failure class on index creation (handleOsBootstrapFailure); this is the write half of it.How
BulkProcessorListener— aggregate a batch's failures into one line per distinct message with its count (23 of 185 item(s) in this batch — …), keep per-item detail atDEBUG, and emit a single actionableERRORwhen the batch was rejected in full for a systemic cause: it names the cause and remediation, states that the shadow store is diverging and must not be promoted, and keeps the verbatim rejection for support. Rate-limited byREINDEX_SHADOW_FAILURE_ESCALATION_MINUTES(default 10) — the throttle is static because a fresh listener is built per batch.OSIndexAPIImpl— classify a bare failure message, not only an exception chain (a rejected bulk item carries text, never a status code), and recognise the security plugin's own wording (security_exception,no permissions for). The matching core is now shared by both entry points, soclassifyConnectionError(Throwable)behaviour is unchanged.IndexConfigHelper— neutral seam (systemicFailureRemediation) so the reindex listener escalates without importing the OpenSearch adapter's classifier.What deliberately does NOT happen
Testing
Tests run: 26, Failures: 0, Errors: 0— 10 new inBulkProcessorListenerShadowFailureTest(aggregation, dominant-cause selection, and every case that must not escalate), 3 new inOSIndexAPIImplConnectionClassifyTest(bulk-item message classification). Both are pure-function unit tests: no container, cluster or config, so they belong to no integration suite.Not covered by an automated test: the rate-limit itself (wall-clock dependent) and the log output shape.
Follow-up, not in this PR
The readiness endpoint (#36360) is the natural place to report "the OpenSearch copy is not receiving writes" as a hard blocker for advancing a phase. This PR only makes that state detectable and named in the log.
🤖 Generated with Claude Code