fix(opensearch): run the full reindex after an absorbed OS halt (#36222) - #36933
fix(opensearch): run the full reindex after an absorbed OS halt (#36222)#36933fabrizzio-dotCMS wants to merge 1 commit into
Conversation
In a dual-write phase an OpenSearch permission failure silently downgraded a full reindex into a no-op — on Elasticsearch too. `OSIndexAPIImpl.indexExists` returns false for any failure, so a 403 on the exists probe is indistinguishable from a missing index. That makes `indexReadyOS()` false, then `indexReady()` false, and `fullReindexStart()` takes its `else` branch and runs `initIndex()` instead of the reindex. The request returns HTTP 200, the progress bar completes, and no index is created in either engine — the `Full reindex started` line never even appears. Pressing Reindex a second time works, but only because the first press halted the migration to phase 0: Elasticsearch then advances to a new timestamp while OpenSearch stays behind, and the two engines diverge. - `fullReindexStart` re-evaluates once after `initIndex()` when the migration was halted during it, and runs the reindex the caller asked for. Gated on the halt rather than a bare `indexReady()` re-check, so a fresh install — where `initIndex()` legitimately turns it true — keeps its current behaviour. The reindex body moves to `startFullReindex()`, shared by both paths. - `MigrationHaltReport` keeps the classified cause and its remediation, so the REST layer can say *why* the migration stopped without importing the OpenSearch adapter. Held per JVM, matching the runtime-only phase reset. - `POST /api/v1/esindex/reindex` compares the phase across the call and returns the reason in `messages`. The degradation to Elasticsearch-only is deliberate and unchanged; being silent about it was not. Reproduced against OpenSearch 3.x with a role scoped to a non-matching index pattern: one click now creates the new indices and reports `AUTH_FORBIDDEN` with the cluster-id/role-pattern fix. Tests: `OsBootstrapForbiddenIndexTest` (OpenSearchUpgradeSuite) gains a case asserting the ES suffix is non-empty and the halt reason is recorded — 4/4 green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @fabrizzio-dotCMS's task in 6m 51s —— View job Code Review: full reindex after an absorbed OS haltI read the diff and the surrounding index/REST layers ( The core fix is sound: One correctness concern worth addressing before merge: New Issues
Notes (non-blocking)
Everything else in the diff — the |
What
In a dual-write phase, an OpenSearch permission failure silently downgraded a full reindex into a no-op — on Elasticsearch too.
Found while reproducing QA's third TC-056 report on #36222. QA described it as "the reindex runs, shows progress, and when it finishes nothing happens". That report was right, and the cause is not the OpenSearch half.
Why it happens
OSIndexAPIImpl.indexExistsreturnsfalsefor any failure, so a 403 on the exists probe is indistinguishable from a missing index:indexReadyOS()→ false (the OS user may not even probe those names)indexReady()→ false (phases 1/2 require both engines)fullReindexStart()takes itselsebranch →initIndex()instead of the reindexThe request returns HTTP 200, the progress bar completes, and no index is created in either engine. The
Full reindex startedline never appears.Pressing Reindex a second time works — but only because the first press halted the migration to phase 0. Elasticsearch then advances to a new timestamp while OpenSearch stays behind, and the two engines diverge. The readiness endpoint (#36360) flags that aftermath as
MISSING_COUNTERPART/safeToAdvance: false.newIndexPath[,]— empty[working_…, live_…]How
fullReindexStartre-evaluates once afterinitIndex()when the migration was halted during it, and then runs the reindex the caller asked for. Gated on the halt rather than a bareindexReady()re-check, so a fresh install — whereinitIndex()legitimately turns it true — keeps its current behaviour. The reindex body moves tostartFullReindex(), shared by both paths.MigrationHaltReport(new) keeps the classified cause and its remediation so the REST layer can report why the migration stopped without importing the OpenSearch adapter. Held per JVM, deliberately matching the scope of the runtime-only phase reset.POST /api/v1/esindex/reindexcompares the phase across the call and returns the reason inmessages.What deliberately does NOT change
indexExistsstill swallows 403 asfalse. Distinguishing it would push an OpenSearch permission problem into paths that serve Elasticsearch — exactly what fix(opensearch): degrade OS index-bootstrap failures to ES-only in dual-write phases (#36222) #36783 set out to avoid, for a much wider blast radius.Testing
Reproduced against a real OpenSearch 3.x with the security plugin and a role scoped to a non-matching index pattern (the cluster-id mismatch TC-056 describes, induced through the role because the cluster id is immutable after first boot). One click now yields:
and the response carries:
./mvnw verify -pl :dotcms-integration -Dcoreit.test.skip=false \ -Dopensearch.upgrade.test=true -Dit.test=OsBootstrapForbiddenIndexTestTests run: 4, Failures: 0, Errors: 0, Skipped: 0— one new case inOsBootstrapForbiddenIndexTest(already registered inOpenSearchUpgradeSuite) asserting the ES suffix is non-empty and the halt reason is recorded. It fails before this change.Not covered automatically: the REST
messagespayload, verified by hand against the stack above.Follow-up, not in this PR
The operator still learns nothing from the UI — the message rides in the response envelope, and nothing renders it yet. And the readiness endpoint reports the consequence (
MISSING_COUNTERPART) but recommends "run a full reindex", which cannot work while the role is wrong; teaching it to probe OS permissions would close that loop.🤖 Generated with Claude Code