Reindex failures in dual-write phases do not report which engine failed
@fabrizzio-dotCMS is already working on this.
Since Aug 29, 2026.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Problem Statement
In PHASE_1_DUAL_WRITE_ES_READS and PHASE_2_DUAL_WRITE_OS_READS every write fans out to two
engines, but reindex failure reporting is engine-agnostic. The UI shows only:
380 Content(s) failed to reindex and were skipped
There is no way to answer the questions an operator actually has:
- Did these fail on Elasticsearch, on OpenSearch, or on both?
- Are the same contentlets missing from both engines, or do the engines now disagree?
- Is the index I am about to read from (which depends on the phase) the one missing content?
GET /api/v1/esindex/reindex is the same — {"errorCount": 380, "inFullReindexation": false} — and
GET /api/v1/esindex/failed returns failureReason with no engine attribution.
This is not hypothetical. During a full reindex in phase 1, failures were 100 % asymmetric:
| New index | Docs present (of the 370 affected identifiers) |
|---|---|
cluster_<id>.working_<ts> (ES path) |
0 |
cluster_<id>.working_<ts>.os (OS shadow) |
524 |
cluster_<id>.live_<ts> (ES path) |
0 |
cluster_<id>.live_<ts>.os (OS shadow) |
406 |
(Counts exceed 370 because of multiple language versions per identifier.)
The cause was a serialization limit reached only on the legacy
org.elasticsearch.client.RestHighLevelClient path and not on the OpenSearch 3.3.0 client path
(details in the companion issue on the ES bulk limit). The engine-specific nature of the failure was
invisible: the UI reported a single flat count, and total document counts actively concealed it —
the two working indices differed by only 45 documents overall while ES was missing 524 of these and
holding roughly 569 others that OpenSearch lacked.
The practical consequence: reads in phase 1 come from Elasticsearch, so those 370 contentlets were
unsearchable — while being perfectly present in OpenSearch. Switching to phase 2 would have made
them reappear, and switching back would have made them vanish again, with nothing in the UI
explaining why.
Diagnosing this required manual _count queries against both clusters plus cross-referencing the
indicies table. That should not be the only way to see it.
Related discoverability gaps found while diagnosing
- Failure counts are reported against no particular index generation. The affected contentlets
existed in the previous ES generation (*_20260826165345) but not in the current one
registered inindicies. Nothing indicated the current generation was incomplete. PhaseRouterlogs shadow-write failures at a level set byDOTCMS_SHADOW_WRITE_LOG_LEVEL
(defaultWARN), which is the right idea — but that signal never reaches the reindex
failure surface the operator is actually looking at.
Steps to Reproduce
-
Configure
DOT_FEATURE_FLAG_OPEN_SEARCH_PHASE=1with distinct ES and OS endpoints. -
Introduce a condition that fails on only one engine — e.g. a contentlet whose
body
exceeds 20 MB, which fails the ES bulk path but succeeds on the OS path (companion issue). -
Run a full reindex.
-
Open Maintenance → Index. Observe
N Content(s) failed to reindex and were skippedwith no
engine attribution. -
Call
GET /api/v1/esindex/reindexandGET /api/v1/esindex/failed— neither response
names an engine. -
Query both clusters directly to discover the failures are one-sided:
# note: use identifier_dotraw, not identifier — the analyzed field splits UUIDs on dashes curl -sk -u <u>:<p> "https://<es>:9200/<working_index>/_search" \ -H 'Content-Type: application/json' \ -d '{"size":0,"query":{"terms":{"identifier_dotraw":["<id>", ...]}}}' curl -sk -u <u>:<p> "https://<os>:9200/<working_index>.os/_search" \ -H 'Content-Type: application/json' \ -d '{"size":0,"query":{"terms":{"identifier_dotraw":["<id>", ...]}}}' -
Note that comparing total document counts between the two indices does not reveal the
divergence, because failures in both directions offset each other.
Acceptance Criteria
- Reindex failure reporting attributes each failure to the engine(s) it failed on, in both
the UI andGET /api/v1/esindex/failed. - The UI distinguishes "failed on both engines" from "failed on one" — the latter means the
engines have diverged and a phase switch will change search results. -
GET /api/v1/esindex/reindexreports per-engine error counts rather than a single
errorCount. - Divergence between write engines is detectable without manual cluster queries — e.g. a
per-engine document count or an explicit mirror-consistency check on the current index
generation. (SiteSearchAPI.writeMirrorsInSync()already does this for site-search
indices; content indices have no equivalent.) - Failure counts identify which index generation is incomplete.
dotCMS Version
dotcms/dotcms:trunk, opensearch.version 3.3.0. OpenSearch 1.3.20 (ES-compatible endpoint) +
OpenSearch 3.4.0, PHASE_1_DUAL_WRITE_ES_READS, PostgreSQL 16, ~1.55 M contentlets.
Severity
Medium - Some functionality impacted
Medium — no data is lost by this defect itself, but it hides the divergence caused by others,
which is what turns a recoverable error into an undiagnosed one.
Links
ESIndexResource.java:180—/failed, no engine attributionESIndexResource.java:318—getReindexationProgress, single flaterrorCountPhaseRouter.java:251—logShadowWriteFailure, the signal that never reaches the operator surfaceIndexConfigHelper.java:125—MigrationPhaseSiteSearchJobImpl.java:299/writeMirrorsInSync()— the mirror-sync check that exists for
site search but not for content indices
Freshdesk ticket: NA — found during internal ES→OpenSearch migration testing.
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.
Assessment
This issue has not been assessed yet.