feat(migration): internal migration-readiness endpoint (content + site search) (#36360) - #36849
Conversation
…+ Site Search half (#36360) Internal, non-public pre-phase-change readiness report for support: - GET /api/v1/index/migration/readiness — @hidden (absent from the OpenAPI / API playground) and gated to CMS admins or the migration support role (OS_MIGRATION_INDEX_VISIBILITY_ROLE_KEY, default os_migration_qa); 403 otherwise. Read-only, stateless — every field derived from live index state at request time. - MigrationReadiness (report DTO): current phase + read/write engines + evaluable flag; overall verdict (safeToAdvance / safeToRollback / outOfSyncCount / summary / per-index blockers); and the per-index ES↔OS mirror diff for both mirrored families. - MirrorStatus (shared per-index diff: kind, es/os existence + exact counts, verdict IN_SYNC/MISSING_TWIN/COUNT_DRIFT, recommendation). - SiteSearchMirrorReconciler (recreated from the lost PR3 work) now uses the exact SiteSearchAPI.documentCount (not a 10k-capped search total), so drift on large indices is reported. ContentIndexMirrorReconciler is stubbed for step b. - MigrationReadinessService composes phase + both reconcilers into the verdict: advance is gated on zero out-of-sync in dual-write phases; rollback is unsafe when any index's ES copy is behind its OS twin (a downgrade would drop that delta) — derived from live counts, no persisted state. Unit test (mocked reconcilers, phase via Config): 7/7 — advance/rollback verdicts across phases 0–3, missing-twin and >10k count-drift blocking advance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…36360) Step b of the migration-readiness endpoint (#36360): - ContentIndexMirrorReconciler now real (was a stub): compares the active working and live content indices against their .os counterparts across both engines, phase-independently. IndiciesInfo holds the cluster-prefixed, un-tagged ES name; exact per-engine counts come from each leaf's getIndicesStats() (_stats primaries.docs.count, not the 10k-capped search total), keyed by the cluster-stripped name (ES bare, OS with .os) — strip-then-tag to match. Emits CONTENT_WORKING / CONTENT_LIVE rows with the same missing-counterpart / count-drift verdicts and a reindex recommendation. Reads leaves directly, never the router. - Terminology: renamed "twin" -> "counterpart" across the new readiness code (verdict MISSING_TWIN -> MISSING_COUNTERPART); "mirror" (the feature name) kept. Unit tests: ContentIndexMirrorReconcilerTest (mocked leaves + injected IndiciesInfo) 5/5 — in-sync, missing OS counterpart, count drift, null/absent slots; service test still 7/7. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… test + docs (#36360) Step c of the migration-readiness endpoint (#36360): - Teardown of the role-gated .os reveal in the index portlets (reverts I-4). MigrationIndexVisibility is now purely phase-based: .os indices are hidden in Phases 0/1/2 and shown in Phase 3, for EVERYONE — regular admins never learn a migration is running. The role key is retained only to gate the readiness endpoint, which is now the single source of truth for migration/QA. Both display sinks (IndexResourceHelper.indexStatsList, cmsmaintenance/index_stats.jsp) drop the user argument; ESIndexResource updated accordingly. - Gate coverage: MigrationReadinessResource.isMigrationSupportUser made package-private and unit-tested (CMS admin / role member allowed, non-admin-without-role denied, null user denied, access-lookup failure fails closed) — 5/5. - MigrationIndexVisibilityTest rewritten to the phase-only contract — 5/5. - OPENSEARCH_MIGRATION.md: new "Migration-readiness endpoint" subsection (route, @hidden + role gate, what it reports, exact counts, stateless rollback verdict) and note that the portlets no longer reveal .os by role. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @fabrizzio-dotCMS's task in 16m 18s —— View job Review — migration-readiness endpoint (#36360)
Reviewed against New Issues
Everything else checks out: the |
…#36360) Readiness JSON now reads es:{exists,docCount} / os:{exists,docCount} per index instead of flat esExists/esDocCount/osExists/osDocCount. MirrorStatus gains a nested EngineCopy record; reconcilers, service verdict, and unit tests updated. 22/22 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Id (#36360) Each per-index row now carries the full name as stored on the server in each engine's EngineCopy.physicalName (ES cluster-prefixed, OS additionally .os-tagged), and the report carries the top-level clusterId embedded in those names. Cluster prefix/id are injected (suppliers) so unit tests stay isolated. 22/22 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ss fields (#36360) - Return the readiness model directly (Response.ok(model)) instead of wrapping it in ResponseEntityView — the internal endpoint has no use for the errors/messages/ pagination/permissions envelope. - Field order is now clusterId, phase, content, siteSearch, verdict. - Renamed contentIndices -> content, siteSearchIndices -> siteSearch. 22/22 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…JSON (#36360) - content and siteSearch are now JSON objects, not arrays: content keyed by slot (WORKING/LIVE), siteSearch keyed by logical index name — self-documenting and directly addressable. - kind is excluded from the payload (@JsonIgnoreProperties on MirrorStatus); it is kept internally only to derive the content slot key and the blocker labels. 23/23 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y slot (#36360) Site Search is an open set with no natural key, so keying it by index name only duplicated indexName. It reverts to a list; content stays a keyed object (WORKING/ LIVE — a fixed pair). The asymmetry mirrors the semantics. 23/23 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Problem
Support/QA had no single place to answer "is it safe to change the OpenSearch migration phase right now, and if not, what do I fix?" before promoting. QA Round 2 on #36360 showed the failure mode: promoting a phase while an index's ES and OpenSearch copies were out of sync led to silent empty/partial results, and the downgrade case (Phase-3-only content invisible after a rollback) was undocumented.
This adds an internal, read-only migration-readiness endpoint that condenses that status with actionable recommendations, and retires the role-gated
.osreveal in the index portlets in favor of it.What this adds
GET /api/v1/index/migration/readiness— internal, read-only, advisory. Never mutates anything; the fix is always the operator re-running the crawl/reindex, which self-heals through the existing write-path gates (#36797 read half, #36825 write half).@Hidden(absent from the OpenAPI / API-playground schema) and gated to CMS administrators or members of the migration support role (OS_MIGRATION_INDEX_VISIBILITY_ROLE_KEY, defaultos_migration_qa); anyone else gets a 403.working/live) and Site Search indices.contentis a keyed object (WORKING/LIVE— a fixed pair);siteSearchis a list (an open set). Each entry is a per-index ES↔OS diff:{indexName, es:{exists,docCount,physicalName}, os:{exists,docCount,physicalName}, verdict, recommendation}, verdictIN_SYNC/MISSING_COUNTERPART/COUNT_DRIFT.physicalNameis the full name as stored on each server (cluster-prefixed;.os-tagged on OpenSearch), and the report carries the top-levelclusterId.evaluable;safeToAdvance(toward OpenSearch-only) andsafeToRollback(downgrade) withoutOfSyncCount, a humansummary, and per-indexblockers. Field order:clusterId, phase, content, siteSearch, verdict.ResponseEntityViewenvelope), serialized by Jackson from typed records.SiteSearchAPI.documentCount; the content half reads each engine leaf'sgetIndicesStats()(index_statsprimaries.docs.count) — never a search total (capped at 10,000, which would hide drift on large indices). Both reconcilers query the two engine leaves directly (not the phase-aware router), so both sides show in every phase.safeToRollbackneeds no history: a downgrade routes reads back to Elasticsearch, so it is unsafe when any index's ES copy is behind its OpenSearch counterpart — derivable from the same snapshot, nothing persisted.Teardown — index portlet visibility is now phase-only (reverts I-4).
MigrationIndexVisibilityno longer reveals.osby role:.osindices are hidden in Phases 0/1/2 and shown in Phase 3, for everyone. The role key is retained only to gate this endpoint, which is now the single source of truth for migration/QA.Layers
com.dotcms.content.index.migration:MirrorStatus(shared per-index diff),SiteSearchMirrorReconciler,ContentIndexMirrorReconciler,MigrationReadinessService(composition + verdict),MigrationReadiness(report DTO). Pure, no mutation.com.dotcms.rest.api.v1.index.MigrationReadinessResource: the JAX-RS resource + role gate.Tests — 23 unit green
MigrationReadinessServiceTest(8): advance/rollback verdicts across phases 0–3, missing-counterpart and >10k count-drift blocking advance, content-keyed-by-slot / siteSearch-as-list, clusterId.ContentIndexMirrorReconcilerTest(5): in-sync, missing OS counterpart, count drift, null/absent slots, physical names.MigrationReadinessResourceTest(5): role gate — admin/role-member allowed, non-admin-without-role denied, null user denied, access-lookup failure fails closed.MigrationIndexVisibilityTest(5): rewritten to the phase-only contract.Remaining test (flagging): a container integration test that drives HTTP 403/200 through the JAX-RS stack is not included yet — the gate decision and both reconcilers are unit-covered. Happy to add it if you'd like it before merge.
Notes
issue-36360-sitesearch-mirror-reconciliation) because it consumesSiteSearchAPI.documentCountfrom that PR. Based against that branch so the diff shows only the readiness changes; will retarget tomainonce fix(sitesearch): reconcile ES/OS index mirrors on the write path (#36360) #36825 merges.docs/backend/OPENSEARCH_MIGRATION.md.🤖 Generated with Claude Code