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 11m 34s —— View job Code Review — migration-readiness endpointReviewed the readiness layers ( New Issues
Resolved
Notes (non-blocking, not flagged)
No blocking issues. The one Medium is the error-path asymmetry above — worth confirming before merge given this endpoint's role during migration troubleshooting. · |
…#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>
…e content indices (#36360) An empty content-index set made the readiness verdict pass vacuously (outOfSync empty -> safeToAdvance true). Treat a missing/ES-copy-less WORKING or LIVE slot as a hard blocker in phases 0/1/2; Site Search stays optional. Also rename PhaseInfo.evaluable -> dualWrite for clarity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…endpoint (#36360) The gate was admin OR role; a plain CMS admin could read the migration report. Change to admin AND the migration support role (OS_MIGRATION_INDEX_VISIBILITY_ROLE_KEY) — a plain admin without the role is now denied, so regular/other admin users never learn a migration is running. Fail-closed on any access-lookup error. Gate test updated to the AND semantics (5/5); docs aligned (also fixes a stale evaluable->dualWrite mention). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Tick the box to add this pull request to the merge queue (same as
|
…36360) Each index now carries driftPercent: the signed % the OpenSearch (mirror) doc count deviates from the Elasticsearch (original) — 0 when equal, negative when the mirror is behind, positive when ahead, -100 for a missing mirror, null when a count is unknown. Derived on MirrorStatus and serialized via @JsonProperty. Tests assert -20% drift and -100% missing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dpoint (#36360) @Schema on MirrorStatus.driftPercent explains how to read the signed % (0 in sync, negative = mirror behind, positive = ahead, -100 empty, null unknown) and @operation on the resource documents the whole response shape + verdict semantics in-code. (@hidden still keeps it out of the public playground.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three non-blocking findings from the PR review: - Drop `SiteSearchMirrorReconciler.canEvaluate()` — dead code with no callers; the phase interpretation already lives in MigrationReadinessService. There is no `evaluable` field on the report (the PR body claimed one). - `outOfSyncCount` is now phase-aware: in Phase 0 the OpenSearch counterparts have not been built yet (they are created during dual-write), so a missing OS copy is the expected state and no longer inflates the count next to a "nothing to reconcile yet" summary. Any other mismatch (OS index with no ES source, drift between two existing copies) stays counted and is now named in the Phase 0 summary. - `safeToRollback` is fail-safe on unmeasurable counts: an unknown count (-1) on either engine is no longer compared numerically — with es=100/os=-1, `100 < -1` returned a false green while OpenSearch may hold more documents. Mirrors the drift verdict's fail-safe stance. Tests: 31 unit green (MigrationReadinessServiceTest 12 -> 16). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks — all three findings were real. Addressed in e6ae14c. 1. 2. Phase 0 3. Tests: 31 unit green ( |
4e551ab
into
issue-36360-sitesearch-mirror-reconciliation
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.dualWriteflag;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 — 31 unit green
MigrationReadinessServiceTest(16): advance/rollback verdicts across phases 0–3, missing-counterpart and >10k count-drift blocking advance, content-keyed-by-slot / siteSearch-as-list, clusterId,driftPercent, the phase-awareoutOfSyncCount(Phase 0 expected-missing excluded, other mismatches still counted), and rollback fail-safe on unmeasurable counts.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
This PR fixes: #36360