Spec: Phase 2 ES read fallback never fires for content search (#37413) - #37438
Spec: Phase 2 ES read fallback never fires for content search (#37413)#37438fabrizzio-dotCMS wants to merge 1 commit into
Conversation
…37413) Issue-resolution spec for #37413. In Phase 2 the documented OpenSearch-> Elasticsearch read fallback never fires for content search: the five read call sites of ESContentFactoryImpl.indexOperationsDelegate() invoke the selected provider directly, so PhaseRouter.read/readChecked — where the fallback lives — is never reached. Root cause was verified against the tree rather than taken from the issue body, which attributes the observed 200/total=0 to the OpenSearchException -> ERROR_HIT branch. A ConnectException is not an OpenSearchException, so that branch is never reached in the reported case. The spec records the verified mechanism instead (count-first cache key, ContentUtils catch(Throwable), ContentHelper resultsSize overwrite) and flags the issue body for correction. Spec only — no implementation. /speckit-plan follows once this is approved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @fabrizzio-dotCMS's task in 1m 40s —— View job Spec review — Phase 2 ES read fallback (#37413)I verified every code reference the spec cites against the current tree. All of them are exact, which is unusual and worth calling out:
Layer 1 is proven. The routing gap is real and the fallback machinery genuinely exists and is genuinely unreachable from this path. Answering the two questions you asked reviewers to focus on: Are the two deferred decisions the right things to defer? — Yes, both.
Is the fix scope bounded correctly? — Mostly, with three tensions the plan should resolve:
Minor
None of these block spec approval — they're refinements for · |
|
Review: Spec's solid, root cause checks out against the tree. One thing I'd want resolved before approving: AC-005 requires the missing-index case to fall back too, but that only works if we touch the ERROR_HIT branch, which the PR itself treats as optional, droppable if the caller list gets too big. I think we should pick one: either AC-005 becomes conditional, or that change stops being optional. |
What this is
PR 1 of 2 in the Spec-Kit flow — carries
spec.mdalone, no implementation. Needs a devapproval (not a merge) before
/speckit-planruns. Implementation lands in PR 2, branchedoff this one.
Spec:
specs/37413-phase2-read-fallback/spec.mdThe defect in one line
In Phase 2, if OpenSearch goes down, content search does not fall back to Elasticsearch as the
migration design promises — because it never passes through the
PhaseRouterthat implementsthat fallback — and returns
200with zero results or a500, while Elasticsearch holds acomplete, in-sync copy the whole time.
Severity High: this is the safety net Cloud and Support are currently being told Phase 2
has. For content search that wiring was never written, so it also blocks recommending Phase 2.
Not a regression. With its read engine down dotCMS has always behaved this way — Phase 0
with ES down does the same thing through the same legacy code. What is broken is the promise.
Root cause — verified against the tree, and a correction to the issue
Layer 1 (the actual defect).
ESContentFactoryImpl.java:273picks the engine with a bareternary and calls the chosen provider directly. Its five read call sites —
:1352search,:1607indexCount,:1616searchHits,:1634indexSearchScroll,:1669createScrollQuery— have no router in between;grep PhaseRouterin that file returnsnothing.
PhaseRouter.read(:187-199) andreadChecked(:298-310) implement the fallbackcorrectly and are simply unreachable from this path. The stack proves it by omission: no
PhaseRouterframe, where the same outage throughIndexAPIImpldoes carry one.Layer 2 — corrects the issue body. #37413 attributes the
200/total=0to theOpenSearchException → ERROR_HITbranch. That is wrong for the observed case: aConnectExceptionis not anOpenSearchException, so it never reaches that branch. The realmechanism, all three confirmed in the current tree:
ContentHelper.java:310counts first, and aCountRequestcarries no offset/limit — sovarying the offset does not change the count cache key. Types queried before the outage had
a cached count; types not previously queried threw → 500.
ContentUtils.java:302catch (Throwable)logs a one-line-truncatedWARNand returns anempty list, swallowing the search failure.
ContentHelper.java:318if (contentlets.isEmpty() && offset <= resultsSize) { resultsSize = 0; }overwrites the real cached count with
0→ 200 /total=0.I will correct the issue body to match before implementation so the two do not disagree.
Two decisions deliberately deferred to
/speckit-planNot clarification gaps — either answer satisfies the design, as long as it is deliberate and
documented:
indexSearchScroll/createScrollQueryholdengine-specific cursor state; a half-consumed OpenSearch scroll cannot resume on
Elasticsearch. Likely propagate-only, documented in code. (Regression Risk, AC-002)
ERROR_HITtouched in the implementation PR, or does the routing fix ship alone?Making the OpenSearch read throw where it now returns an empty result changes the contract
for every caller that depends on receiving empty instead of an exception. Those callers must
be enumerated first; if the list is large, Layer 1 stands on its own. (Fix Scope, AC-006)
Explicit non-goals
ContentFactoryIndexOperationsESkeeps itsERROR_HITsemantics; phases 0 and 1 untouched.
ContentUtils:302andContentHelper:318arepre-existing and affect all phases including pure Elasticsearch; fixing Layer 1 removes the
Phase 2 case. Separate issue, not folded in.
IndexAPI<F>generic parameterization — its own PR.behavior.
Reproduction note worth reading
The bug hides from QA. Re-running the same query body returns
200with correct totals —the query cache serving pre-outage results, which looks exactly like a working fallback. Only
novel, uncached queries expose it, which is why the repro varies
offseton every call.Design being violated
docs/backend/OPENSEARCH_MIGRATION.mdlines 105, 402, 676 and 901 all state the Phase 2 readfallback as designed behavior.
Review focus
The spec, not code — there is none yet. Specifically: is the fix scope bounded correctly, and
are the two deferred decisions the right things to defer?
🤖 Generated with Claude Code
This PR fixes: #37413