Skip to content

Add explicit Elasticsearch index compatibility upgrades - #307

Open
niemyjski wants to merge 87 commits into
mainfrom
feature/elasticsearch-index-compatibility-upgrades
Open

niemyjski wants to merge 87 commits into
mainfrom
feature/elasticsearch-index-compatibility-upgrades

Conversation

@niemyjski

@niemyjski niemyjski commented Aug 3, 2026 •

Copy link
Copy Markdown
Member

Status — audit fixes committed; final release validation pending

Current head: 9c681f8ce11b10509aadd2af9dcbbdcaea33200d. Four clean audit-fix commits have been fast-forwarded onto this PR. They have Release-build, red/green regression, core-suite, and packaged-consumer evidence. Production approval is still gated on final-head CI and the remaining full-suite/persistent-chain results.

Base main remains a61d541613528428cc00bc2236602db72948a924, already included in this branch. A fresh compare after promotion reports 0 commits behind main. There was no newer base revision to merge. No force-push, merge to main, release, PR-base retargeting, or change to this PR's publishing workflow was performed.

Temporary execution workflows remain exclusively on audit/pr-307-final-validation-20260922; do not merge that audit branch. The four promoted commits change only production source, tests, and documentation.

Summary

Add explicit, operator-invoked APIs to inspect and remediate Elasticsearch index-format compatibility independently of schema versioning. This is offline maintenance, not an automatic startup migration. Normal ConfigureIndexesAsync performs zero compatibility requests.

  • Discover owned physical indexes using a server-info request plus one aliases/settings request per configured index, independent of partition count.
  • Recreate an incompatible index as reindexed-v{server-major}-{canonical-name}, verify the copy behind write blocks, atomically transfer complete alias definitions, and delete the old physical index.
  • Preserve repository-facing aliases and custom naming hooks without adding reindexed-v* to normal schema, mapping, maintenance, cleanup, or deletion patterns.
  • Public recovery finishes only marked, committed cutovers. Ambiguous, unmarked, foreign, or contradictory evidence requires manual reconciliation.
  • Keep the persistent Elasticsearch 7→8→9 test explicitly environment-gated for release rehearsal, rather than running the expensive chain in ordinary CI.

Final-audit fixes

Commit Change
54856100 Validate wire counters independently of application serializer naming policy; reject timed-out copy results.
8915594e Authenticate error-index lineage without stripping natural compatibility-looking prefixes first.
1d76e627 Require positive task-termination evidence and preserve original cancellation through secondary recovery failures.
9c681f8c Document strict task-result, termination, and cancellation evidence.

Task results cannot silently lose failure evidence

The shared reader previously ignored timed_out, coerced dictionary counters through Convert.ToInt64, and delegated JSON counter binding to the application serializer. Timeout evidence could be discarded; booleans or fractions could become counters; and application naming policy could lose wire values.

The reader now parses all six counters directly and requires nonnegative, in-range integers. Malformed present timeout flags are rejected. timed_out: true is rejected before compatibility cutover or normal-schema alias promotion. Tests cover JSON/dictionary payloads, supported integer types, malformed values, custom naming policy, buffered/unbuffered transport, and both callers. This does not supply #327's broader schema-completion guarantees.

Natural names cannot bypass error-index provenance

IsPotentialCompatibilityErrorName now checks the configured native name before considering a generated wrapper. Recovery therefore requires the persistent .foundatio-reindex-error marker for natural reindexed-v… families too. Unit and public inspection regressions cover plain, versioned, daily, monthly, and literal reindexed families, while retaining ordinary names that naturally end in -error.

Task 404 is uncertainty, not proof that copying stopped

Task lookup can fall back to stored results when the owning node is unreachable; a missing stored result is not positive termination evidence. See the pinned Elasticsearch task lookup implementation.

After a copy has been dispatched, cleanup now requires a successful task response explicitly reporting completed: true. A 404 does not invoke the termination callback that authorizes deleting the destination or releasing the source write fence. Tests cover clean/partial cancellation, positive completion, runner cleanup authority, and the full upgrade path. Existing positive-termination fixtures now supply explicit completion rather than treating 404 as success.

Secondary recovery errors do not hide cancellation

The upgrader follows the original operation failure through the first aggregate cause, preserving its cancellation token if inspection/reset also fails. Secondary diagnostics remain attached. An independent cleanup-token timeout does not turn a non-cancellation operation failure into apparent caller cancellation. Committed-cutover success paths remain separate.

Validation — observed results and remaining gates

Verified candidate evidence: macOS run 35771563588, which generated the exact four promoted commits:

  • Original source plus the 61 new regression cases: Release build 0 warnings/errors; 37 failed, 24 passed, 0 skipped.
  • Fixed source: Release solution build 0 warnings/errors; libraries compile for .NET 8 and .NET 10.
  • 61/61 new regression cases passed, with no skips.
  • 152/152 core tests passed, with no skips.
  • Both actual local NuGet packages were created. External consumers built for .NET 8 and .NET 10 with nullable warnings treated as errors and executed successfully on both runtimes. The consumer exercises external subclass hooks and the configuration compatibility API.
  • Candidate staging verified a clean worktree, no .github delta, and no audit harness files in the promoted tree.
  • Artifact 10714278739 contains the candidate patch/bundle, exact commit IDs, build/test logs, and packaged-consumer results.

The same 37-red / 61-green / 152-core results were independently observed on Windows in run 35770100836. That run's consumer restore failed because its audit harness treated the HTTPS package-source argument as a local Windows path; the replacement uses NuGet.Config. This was not a product-test failure.

The original formatter command returned zero but reported optional sibling-project workspace failures. That is not accepted as clean formatter evidence. A standalone folder-mode whitespace verification is included in the refreshed gates. git diff --check passed during candidate construction and staging.

Remaining gate Run / last verified state
Final-head ordinary CI, 9c681f8c 35773601794, queued at last check.
Full solution on Elasticsearch 8.19.15 and 9.5.0 Matrix jobs in 35770775846, queued at last check.
Standalone formatting and Windows packaged consumers 35772821630, queued at last check.
Corrected isolated persistent chain 35773290205, queued at last check.
Combined current #327/#307/#308 revisions Not validated.

What the first persistent rehearsal established

Run 35770775846 built the candidate cleanly, preserved a real ES7 data volume through ES8, executed the explicitly gated compatibility test on ES8 (1 passed, 0 skipped), restored snapshots at ES7 and ES8, and passed alias read/write smoke checks at ES8.

The ES9 leg did not execute. Its startup log identifies an unrelated, ES7-created .geoip_databases system index as incompatible. The Foundatio-owned index had already been upgraded. Artifact 10714821867, especially chain-final-server.log, preserves the exact failure and successful earlier phases.

This demonstrates why a Foundatio-owned index preflight is not cluster-wide upgrade certification. The replacement isolated rehearsal disables the optional GeoIP downloader from initial disposable-cluster provisioning; it does not delete or mutate production/system data to conceal the failure. Production clusters still require Elastic Upgrade Assistant and system-index readiness checks.

The rehearsal covers a persistent single-node 7.17.29→8.19.15→9.5.0 sequence and same-major snapshot restoration. It is not a rolling multi-node test or proof of arbitrary downgrade safety. Earlier historical chain/combined-snapshot results remain in the discussion; they do not substitute for these current release gates.

Existing review fixes retained

The prior review merged main, repaired the direct hashing dependency and xUnit 4 namespaces, and added 20 passing destination-reservation cases. All registrations reserve their logical/native/error names and configured/dated aliases, including absent siblings excluded from the upgrade batch; candidates are checked again under the reindex lock. The P1 collision thread was answered and resolved.

The public guide, XML remarks, and agent lifecycle reference also distinguish stable alias routing and count checks from guaranteed lossless schema migration, remove nonexistent force: true examples, and use the correct named configuration-level progress callback.

The old ordinary CI at 13f069f6 is green; the earlier 97a335f3 run reported 1,107 passed, 0 failed, 1 deliberate manual-chain skip. Those results are historical baseline evidence, not full-suite evidence for the four new commits.

Lifecycle and operating contract

Validate the complete requested batch before mutation, then revalidate under reindex:{logical-name}: registration, ownership, destination reservations, throttles, schema precedence, duplicate lineage, and supported source state.

The workflow marks/write-blocks the exact source, refreshes it, creates/marks the deterministic target through _create_from, and copies using op_type=create, pipeline _none, one unsliced task, and deterministic X-Opaque-Id. It blocks the target before checking task/shard/count/mapping/settings/alias evidence, restores temporary settings, re-reads source evidence, and atomically deletes the source while transferring original/canonical aliases. It unblocks the target before removing the final workflow marker.

Once cutover dispatch starts, automatic reset is forbidden for that attempt. A timeout does not prove the request failed to commit. Public inspection is read-only; public recovery takes the same lock and only finishes a marked committed target. None, Wait, Finish, and ManualIntervention report distinct observed states. There is no public force-unblock/reset operation. Only the running attempt may clean up acknowledged pre-copy setup or a positively terminated exact task, with intact marked topology and before any cutover dispatch.

Ordinary callback errors are logged; cancellation/lock-renewal failures follow evidence-based handling. Batches are not transactional, and cancellation may be reported after a committed cutover. Unsupported system/dot, data-stream, ILM, CCR, closed, non-standard-mode, source-disabled/source-filtered, or pre-blocked sources are rejected. _create_from requires Elasticsearch 8.18+; generated names over 255 UTF-8 bytes are rejected, not truncated. Kibana can choose the same namespace, so the workflows must not run concurrently against one source.

Merge order and stacked PRs

Recommended release order: finish and land #327's normal-schema data-loss work first, then merge updated main into #307 and deliberately reconcile the shared code. At the audit's last read, #327 was 091b2ba0a6c24806568c42897c1078f0b00dae5c; re-read tips before integration.

A temporary #327→#307 review stack is useful when both must remain in parallel development. It is not mandatory when #327 can land first. Merely changing the base is insufficient: reconcile full-source copy, completion/error propagation, quiesced reconciliation, alias preservation, compatibility ownership, and strict task evidence, then test the actual combined tree. After the parent lands, retarget/update the child against main.

Keep #308 independently reviewable unless it gains a real dependency. Its shared mapping-discovery conflict must preserve explicit external mapping patterns, canonical ownership, hidden/error filtering, and deterministic ordering. Shared files alone do not require stacking every PR. No related branch was merged or retargeted during this audit.

Production release gate

Require final-head CI, full Elasticsearch 8/9 suites, the completed persistent sequential-major/snapshot rehearsal, clean formatting, and the intended combined release-tree validation before approval.

For deployment: stop writers, queue consumers, maintenance jobs, and alias managers; verify a restorable pre-upgrade snapshot; close the old-major rollback window; run the explicit operation; verify aliases and reads/writes; drain/restart clients before writes resume because cached concurrency tokens belong to deleted indexes; then take a new verified snapshot. Use Elastic Upgrade Assistant for readiness beyond Foundatio-owned indexes.

Green tests are scoped evidence, not a guarantee that every possible bug has been eliminated.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 76b9eaff66

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Foundatio.Repositories.Elasticsearch/Configuration/Index.cs Outdated
@niemyjski niemyjski self-assigned this Aug 4, 2026
@niemyjski
niemyjski force-pushed the feature/elasticsearch-index-compatibility-upgrades branch from 76b9eaf to 4226b37 Compare August 5, 2026 21:30

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4226b374e5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Foundatio.Repositories.Elasticsearch/Repositories/ElasticReindexer.cs Outdated
Comment thread src/Foundatio.Repositories.Elasticsearch/Repositories/ElasticReindexer.cs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fd97a54e66

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a664a60d98

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Foundatio.Repositories.Elasticsearch/Configuration/DailyIndex.cs Outdated
Comment thread src/Foundatio.Repositories.Elasticsearch/Configuration/VersionedIndex.cs Outdated
Comment thread src/Foundatio.Repositories.Elasticsearch/Configuration/VersionedIndex.cs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92a9507c4a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Foundatio.Repositories.Elasticsearch/Repositories/ElasticReindexer.cs Outdated
Comment thread src/Foundatio.Repositories.Elasticsearch/Repositories/ElasticReindexTaskRunner.cs Outdated
Comment thread src/Foundatio.Repositories.Elasticsearch/Configuration/CompatibilityIndexName.cs Outdated
Comment thread src/Foundatio.Repositories.Elasticsearch/Repositories/ElasticReindexTaskRunner.cs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba6c9bf746

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Foundatio.Repositories.Elasticsearch/Configuration/CompatibilityIndexName.cs Outdated
Comment thread src/Foundatio.Repositories.Elasticsearch/Jobs/CleanupIndexesJob.cs Outdated
Comment thread .github/workflows/build.yml Fixed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3fcfe71466

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Foundatio.Repositories.Elasticsearch/Repositories/ElasticReindexTaskRunner.cs Outdated
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

Explain registered destination reservations and callback cancellation behavior. Correct nonexistent force-configuration and positional progress-callback examples, distinguish best-effort schema resume from compatibility recovery, and avoid a timeless preview label for the server API.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

Document registered destination reservations and the actual progress-callback contract. Fix progress-callback call syntax and remove the nonexistent force configuration option. Separate stable alias routing and best-effort schema catch-up from the offline compatibility protocol, including task lifetime, interrupted copies, retained sources, and disk headroom.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

Remove an unrelated constructor statement reorder from the compatibility reservation edits. Keep the review diff focused on destination validation.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@niemyjski niemyjski left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review follow-up completed

Pushed 11 commits, merged current main without rewriting history, repaired the baseline build, fixed the registered-destination P1, added 20 regression cases, and corrected the API/public/agent documentation. The destination-collision thread now has implementation and CI evidence and is resolved; a fresh thread listing shows no unresolved threads. The PR description separates current evidence from historical validation and records the #308/#327 integration risks.

Verified: Release solution build on 97a335f with 0 warnings/errors; libraries target .NET 8 and .NET 10; .NET 10 test run reports 1,107 passed / 0 failed / 1 skipped in run 35744890739. All 20 new destination cases are included. Main a61d541 is an ancestor of the head, with 0 commits behind.

Final head: 13f069f has passed its build step in run 35748460623; its test step is still running at this review. Do not substitute the preceding green run for a final-head result.

Release validation still required: the skipped test is the deliberately gated persistent Elasticsearch 7→8→9 sequence. This follow-up did not execute that chain, snapshot restoration, packaged external consumers, or combined current #307/#308/#327 tips. Re-run the persistent sequence and restoration on the intended release candidate, and revalidate any later combined-PR conflict resolution. Normal schema migration remains distinct from this offline compatibility protocol; the docs no longer promise guaranteed crash recovery or lossless concurrent catch-up.

This review does not approve, merge, publish, or certify a production deployment. The code fixes and docs are committed; final CI and the explicit release rehearsal remain gates.

Copy link
Copy Markdown
Member Author

Final-audit findings — validation in progress

The previously pending final-head CI on 13f069f60af1b406ee4dafa4874c1793b5caafba is now green. main still resolves to a61d541613528428cc00bc2236602db72948a924, which is already included in this branch; there is currently no additional base commit to merge.

The deeper audit found additional issues, so the old green build is not final production approval:

  1. The shared task-response reader ignores timed_out, coerces malformed dictionary counters, and lets application serializer naming policy affect wire counters. A candidate now reads validated nonnegative integral counters directly and refuses timed-out copies before compatibility cutover or normal-schema alias promotion.
  2. IsPotentialCompatibilityErrorName strips a natural reindexed-v… prefix before recognizing the configured native name. This can omit the persistent error-index provenance check during recovery. The candidate checks the native name first and adds public inspection regressions for plain, versioned, daily, monthly, and literal reindexed naming.
  3. Cancellation treats task HTTP 404 as proof of termination. Elasticsearch's task lookup can fall back to stored results when the owning node is unreachable, so missing results are not positive proof that copying stopped. The candidate keeps this outcome uncertain and requires a successful completed:true task response before cleanup. Original cancellation remains distinguishable even when inspection/reset also fails; an independent cleanup timeout must not invent caller cancellation.

Candidates and regression probes are isolated on audit/pr-307-final-validation-20260922; they have not yet been promoted to this PR. Audit run 35768612415 includes red/green regression validation, full suites on Elasticsearch 8.19.15 and 9.5.0, external packaged .NET 8/.NET 10 consumers, and a persistent 7.17.29→8.19.15→9.5.0 chain with snapshot restores and alias read/write checks. These jobs are queued, not passed. An earlier audit-only fixture compile error was corrected, and the harness now propagates piped build/test exit codes explicitly.

For release integration, prefer landing #327's normal-schema data-loss fixes first, then reconcile #307 against that updated base. A temporary #327→#307 review stack is useful while both remain open, but merely changing the PR base is not a semantic merge or combined test. #308 should remain independently reviewable unless it gains a real dependency; its shared mapping-discovery conflict still needs an explicit resolution and fresh combined validation. No PR base was retargeted, no PR was merged, and publishing behavior on this PR is unchanged.

Copy link
Copy Markdown
Member Author

Final-audit evidence update

The new candidate now has actual regression proof from Windows run 35770100836:

  • Original source plus new tests: Release build 0 warnings/errors; 37 failed, 24 passed, 0 skipped out of 61 new cases.
  • Candidate source: Release build 0 warnings/errors; 61/61 new cases passed, no skips; 152/152 core tests passed, no skips.
  • Both local NuGet packages were created. External-consumer restore hit an audit-harness Windows source-URL parsing problem; the harness now uses NuGet.Config. The prior formatting command also reported optional sibling-project workspace failures despite exit 0, so that is not accepted as clean formatting evidence; the new gate uses standalone folder-mode whitespace verification.

The persistent rehearsal in run 35770775846 built the same candidate cleanly, preserved a real ES7 data volume through ES8, ran the explicit compatibility chain test on ES8 (1 passed, 0 skipped), and completed snapshot restoration at ES7/ES8 and alias read/write checks at ES8. The ES9 leg did not run. The node's captured fatal error identifies an unrelated ES7-created .geoip_databases system index as incompatible; it does not identify the successfully upgraded Foundatio index.

This is concrete evidence for the documented boundary: a Foundatio-owned index preflight is not cluster-wide upgrade certification. System-index readiness still belongs to Elastic's Upgrade Assistant/tooling. Artifact 10714821867 contains the exact fatal startup log (chain-final-server.log) and per-phase results. The replacement isolated rehearsal disables the optional GeoIP downloader from initial disposable-cluster provisioning, rather than deleting or modifying production/system data to hide this result. No user cluster is involved.

The fixes still need clean-source promotion and completed integration/package/chain gates. Do not merge the temporary audit branch or treat the failed/queued jobs as passed. The original #307 source head remains 13f069f6 until a reviewed clean candidate is promoted.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ niemyjski
❌ github-actions[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

@niemyjski niemyjski left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final-audit fixes are now on this PR in four clean commits: 54856100, 8915594e, 1d76e627, and 9c681f8c. The promoted diff contains only source, regression tests, and documentation; no audit workflow or publishing changes.

Verified evidence for this exact candidate: Release solution builds for net8.0/net10.0 with zero warnings/errors; 37 of 61 new regression cases fail on the original source, all 61 pass with the fixes; all 152 core tests pass; actual local NuGet consumers build with nullable warnings treated as errors and execute on .NET 8 and .NET 10. See run 35771563588 and its candidate patch/bundle artifact. The red/green/core results were also observed independently on Windows.

The corrected paths cover discarded task timeout evidence and coerced/lost counters, natural-prefix error-index provenance, task-404 cleanup authority, and original cancellation when secondary recovery fails. Existing positive task-termination fixtures now require completed:true instead of 404.

Approval remains withheld pending the recorded final-head CI, full Elasticsearch 8/9 suites, clean standalone formatting, and complete persistent-chain rehearsal. The first chain successfully migrated the real ES7-created Foundatio index on ES8 and restored snapshots, then ES9 refused an unrelated 7-created .geoip_databases system index. This is retained as evidence that cluster-wide readiness still requires Elastic's tooling; the isolated repeat disables the optional downloader from initial disposable provisioning, not on a user cluster.

Fresh base comparison: main a61d541 is already included, zero commits behind. For the combined release, prefer landing #327 first and then reconciling #307 against main. A temporary #327→#307 stack is useful for parallel review, not mandatory; retargeting alone does not resolve shared reindex semantics. Keep #308 independently reviewable unless a true dependency is introduced, and test its mapping-discovery reconciliation explicitly. No related PR was merged or retargeted.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9c681f8ce1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +485 to +486
string canonicalAlias = canonicalName.ToString();
if (!aliases.HasCanonicalCompatibilityAlias(canonicalAlias))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject sibling-owned canonical aliases before deletion

When a registered VersionedIndex named events coexists with a registered index named events-v2, the sibling's compatibility-upgraded physical index can be reindexed-v9-events-v2 with canonical alias events-v2. Calling events.DeleteAsync() resolves that alias here, accepts the canonical marker, and deletes the sibling's physical index because sibling ownership is checked only in the unwrapped-name branch above. Fresh evidence beyond the previously fixed native-name collision is that this wrapped-name branch never calls IsNativelyClaimedByOtherConfiguredIndex(canonicalName); apply the same arbitration before accepting the canonical alias.

Useful? React with 👍 / 👎.

@niemyjski niemyjski left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deeper consistency review: task absence, writer ownership, and destructive-cutover durability

Reviewed #307 at 9c681f8ce11b10509aadd2af9dcbbdcaea33200d, #327 separately at 091b2ba0a6c24806568c42897c1078f0b00dae5c, and Foundatio's actual 13.0.4 dependency tag (d6d686d31be062ba0561eb210e40ad40d79c8fff). These are source-audit findings and failure schedules, not newly executed distributed fault-injection tests. This review does not approve production release.

Confirmed: the destructive cutover does not require restored redundancy

ElasticIndexCompatibilityUpgrader.CreateTargetAsync temporarily sets replicas to zero. RestoreTargetSettingsAsync restores the configured count, but WaitForTargetHealthAsync accepts yellow, with no initializing/relocating shards, before the atomic request deletes the source. Unassigned replicas satisfy that combination. A replacement with only primary copies can therefore become the sole live dataset even when the configured policy requires replicas.

Require target-scoped health and allocated copies consistent with the restored redundancy policy before source deletion. Green is useful only together with the intended replica configuration; green with zero configured replicas is not redundancy. Explicit zero-replica deployments need an explicit risk policy. Make the health/recovery deadline configurable: an unconditional 30-second wait is not a large-index recovery budget. Test a source with replicas, a target whose replicas cannot allocate, and assert no remove_index is dispatched.

Confirmed upstream dependency gap: renewal can return normally after ownership is lost

The repository references Foundatio 13.0.4. At that exact release, CacheLockProvider.RenewAsync returns the task from ReplaceIfEqualAsync(...).AsTask() as non-generic Task, without checking the Boolean outcome. A completed compare-and-replace returning false therefore does not report loss of ownership to callers. The upgrader awaits reindexLock.RenewAsync, so a non-throwing renewal is not evidence the lease remains owned.

Fix and test this in Foundatio, then integrate the dependency deliberately. Test: owner A acquires; its key expires; B acquires; A renews; renewal must report loss and A must not authorize cutover, reset, or block removal. Separately, renewal only on progress callbacks is insufficient protection against long awaits or a stalled callback. Lease renewal must be independent, and lost ownership must revoke mutation authority. A cache lease, or an epoch stored only in a manifest, does not itself fence an already submitted Elasticsearch request.

The 404 change is correct, but proves only one part of the protocol

Pinned Elasticsearch 9.5 task lookup falls back to stored results when the owner is absent or unreachable. Thus task 404 must remain unknown rather than authorize cleanup. Explicit completion means the observed task terminated; successful migration additionally requires the validated response, stable source/target, correct identities, and durable cutover.

The next fault test must cover duplicate/delayed asynchronous submissions. RunCompatibilityReindexAsync sets X-Opaque-Id but does not locally disable inherited transport retries. An opaque ID is correlation, not deduplication. Under a retrying client/proxy, a first submission can be accepted without a received response and a second can return the task ID being tracked. Completion of that one task does not prove all submissions or stale workers are harmless. This is a conditional risk, not a claim that every default client duplicates submissions. Audit client/resilience/proxy launch retries; retain attempt identity and task lineage; do not delete/reuse a target based only on one returned task ID or an empty listing.

Elasticsearch 9.5 adds a reindex-specific cancellation API that follows node-shutdown relocations and can return final state. Consider it through a tested version capability adapter, not as an unconditional replacement on ES8/earlier ES9. Its documented 404 is still ambiguous. Reference: https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-cancel-reindex

Recovery hardening and availability design

Current public Finish recovery uses names, static workflow markers, canonical alias presence, and task absence; it does not recover a durable record of the original index UUIDs and full expected alias manifest. Under the documented exclusion of other writers/managers, this is a narrower protocol than arbitrary concurrent recovery. For stronger unattended recovery, persist source/target UUIDs, attempt identity, expected alias/mapping/settings/transform fingerprints, task lineage, verified-copy evidence, and cutover intent. Use compare-and-set transitions, but do not mistake them for server-enforced fencing of aliases or bulk writes.

A potential safe failback is to quarantine the private destination with an acknowledged dedicated write block, retain it, and resume the intact source. This is valid only if no cutover request can still execute and the attempt/stale controllers cannot later promote or unblock that target. It is NOT permission to delete an uncertain target, nor safe after ambiguous cutover dispatch. This could reduce outage on pre-cutover failures without pretending a missing task terminated, but requires its own adversarial tests before implementation.

Scope and architecture

Keep #307's block-before-copy contract as the simple strict offline mode. A live pre-copy followed by a complete blocked rescan still has O(dataset-size) outage. A genuinely short final outage requires a durable, ordered change stream including deletes, or migration of demonstrably immutable partitions. Do not import #327's unconditional source-unblock lifecycle into this protocol; that branch has separate findings.

Primary operational references: https://www.elastic.co/docs/reference/elasticsearch/index-settings/index-block ; https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/indexing-speed ; https://www.elastic.co/docs/manage-data/migrate/migrate-data-between-elasticsearch-clusters-with-minimal-downtime .

Copy link
Copy Markdown
Member Author

Consistency follow-up — tracked blockers and implementation scope

At reviewed head 9c681f8ce11b10509aadd2af9dcbbdcaea33200d, production approval remains withheld. This consolidates the deeper review into a checkable implementation ledger; these items are not fixed merely because they are logged.

Shared ownership and task authority

  • Fix Foundatio 13.0.4 CacheLockProvider.RenewAsync silently discarding a failed ReplaceIfEqualAsync result; prove expired/replaced owners cannot renew successfully. Track the upstream fix and consumption separately.
  • Separate lease renewal from potentially slow observer callbacks; prohibit cutover/reset/unblock by an owner that lost its lease. A heartbeat alone is not server-side fencing.
  • Disable inherited automatic retries for asynchronous reindex submission and reconcile unknown launch outcomes without issuing a second blind launch. X-Opaque-Id is correlation, not idempotency.
  • Preserve the existing rule that task 404 is uncertainty, not positive termination. Cover delayed/duplicate launches and relocated tasks; do not infer all writers are gone from one terminal task or an empty task listing.
  • Bind recovery to physical index generations and attempt/cutover evidence; audit stale worker/index-name reuse. No automatic destructive cleanup or failback after ambiguous cutover.

Compatibility replacement safety

  • Require the destination's configured replica policy before deleting its source, rather than accepting yellow with unassigned replicas; make health waiting appropriate for long migrations.
  • Verify private destination ownership, exact copy/shard evidence, and source/target write fences through cutover. Test ambiguous alias responses and stale direct writers.
  • Keep this protocol explicitly offline. Do not move the block later while silently promising delta catch-up, delete propagation, or low downtime.

Related #327 blockers

Track scripted delete resurrection, _mget errors mistaken for absence, unchecked bulk-item/scroll/shard errors, lost routing, scalar multi-shard checkpoints, truncated changed-ID samples, and unconditional retired-source unblocking on #327. Shared-file integration must retain both protocols' safeguards; an independently green PR does not certify the combined tree.

Large-data operating contract

Document strict offline replacement versus a separately designed snapshot/backfill + durable change-capture protocol, including tombstone retention, ordering/idempotence, capacity/backpressure, rollback boundaries, and immutable-partition options. Generic online CDC, arbitrary script reconciliation, and automatic ambiguous-task quarantine must not be represented as implemented without their own tested protocol.

Implementation is being worked on in this session in isolated candidate branches with logical commits and regression tests. Exact promoted SHAs, actual test results, and remaining blockers will be recorded here. No release or merge to main is authorized by this work.

niemyjski commented Sep 22, 2026 •

Copy link
Copy Markdown
Member Author

Consistency hardening tracker — implementation and validation

This remains an open release gate, not production approval. Product baseline is #307 9c681f8c; clean candidates are validated before branch promotion. Temporary scripts/workflows remain only on audit/pr-307-final-validation-20260922 and must not be merged.

Implemented upstream

  • Reproduced silent failed lease renewal and fixed the discarded compare-and-replace result.
  • Found and fixed a second defect: in-memory compare-and-replace could resurrect an expired/released lease.
  • Opened fix: reject renewal of lost and expired cache leases Foundatio#573, clean commit 2f1c51e015b22c684d24fc78a4b80c27235e9ab2: 425 lock/cache tests passed, zero failures/skips; original ownership tests were 3 red / 1 green; Release .NET8/10 build had zero warnings/errors. Evidence: run 35795463344.
  • Land/release and explicitly consume that dependency. Repositories still references Foundatio 13.0.4; a PR in the upstream repository does not change installed packages.

Implemented in the #307 candidate; full validation/promotion pending

  • Destination-specific green health after replica policy restoration, no unassigned/initializing/relocating shards, positive configurable CompatibilityUpgradeHealthTimeout (30-minute default), bounded health polling and correct treatment of poll HTTP408 as not-ready.
  • Independent 30-second heartbeat, bounded renewal attempts, linked work cancellation on lease loss; guarded renewal around callbacks and destructive boundaries. Detected lease loss forbids automatic reset/recovery/unblocking.
  • Effective single-attempt asynchronous submission and alias cutover. The new two-node retry regression exposed that the supported Elastic transport ignores request-local MaxRetries(0); selecting and pinning one normal pool node sets the effective retry count to zero. Correlation headers are not idempotency keys; proxy retries remain an operator constraint.
  • Replica readiness, no-retry, cancellation/lease loss and independent-heartbeat tests. Earlier focused run passed 270 compatibility cases and 152 core cases; the latest heartbeat candidate is being validated separately.

Latest #307 validation: run 35797864520, pinned harness dbfbe4362215b2bac3d0de616ffedbbd91760d82. Its full suites use real two-node ES8.19.15 and ES9.5.0 clusters. The earlier single-node full-suite run correctly failed replica-dependent tests; it is not counted as passing evidence.

Cross-PR implementation

#327 candidate now implements strict routed item/scroll/bulk evidence, exact reconciliation counts, source UUID + complete primary-shard checkpoint vectors, changed-ID pagination past 1,000 results, scripted-quiescence preflight rejection, retained retired-source fences, no completion fabrication from a QuiesceSource flag, single-attempt launch/cutover and independent lease renewal. Tracker: #327 (comment) . Latest full validation is 35797917483; no combined-release guarantee is inferred.

Still open — do not check off on a green ordinary suite

  • Durable generation/attempt-bound migration journal: source/target UUIDs, transform/configuration fingerprints, task lineage, verified state, cutover intent and exact expected alias topology.
  • Stale-controller exclusion and recovery that account for already-dispatched/duplicate/delayed requests across restarts. Heartbeats and task-list snapshots are not storage-enforced fencing.
  • Safe recovery of unknown launches across application-level retries; a transport single attempt alone does not persist the outcome across processes.
  • Version-aware ES9.5 relocated-task cancellation/read/list handling and fault injection. Preserve the current task404-is-unknown behavior for all versions.
  • Final-head CI, refreshed persistent-major/snapshot rehearsal, packaged consumers, and semantic reconciliation of the intended Add explicit Elasticsearch index compatibility upgrades #307/fix(reindex): stop a reindex from silently dropping documents #327/fix: support external-index id opt-out and safe cursor paging (#305) #308 release tree.
  • Low-downtime change-capture/replay is a separate protocol, not supplied by a full blocked rescan. Operating guidance must retain queue draining, routing/tombstone ordering, destination isolation, stale-client retirement and snapshot/rollback constraints.

No production data, main-branch merge, release/publishing policy, force cleanup, history rewrite or PR base retargeting has been performed.

niemyjski commented Sep 23, 2026 •

Copy link
Copy Markdown
Member Author

Consistency remediation tracker — sequential execution

This is the master tracker for #307, #327, and the upstream lock repair. Not a production approval. Each entry is closed only with implementation and exact revision/test evidence. Existing newer work is retained rather than overwritten.

Observed progress

  • Provider repair already exists in Foundatio #573 at c4fbcd4c173969b5560f88954c54ae62307f7a95. Artifact 10725336070 from run 35799107410 confirms 427 passing lock/cache tests and two red baseline CAS regressions. Repositories still references 13.0.4: release/consumption is an open dependency, not silently substituted.
  • Add explicit Elasticsearch index compatibility upgrades #307 candidate 3af1fccb513a4bfaa2c4c6fcaf468c2de7529c69 reuses the reviewed lease-supervision/replica-health candidate and adds tested node-predicate preservation. Run https://github.com/FoundatioFx/Foundatio.Repositories/actions/runs/35804332376: Release builds 0 warnings/errors; 2 red node-selection cases; 21 focused cases green; 1,187 passed, 0 failed, 1 deliberate persistent-chain skip on each two-node ES8.19.15 and ES9.5.0 cluster. Artifact 10726804580 preserves exact clean commits and logs. Not yet promoted while the ordinary-CI two-node profile is validated.
  • Duplicate dispatch is now an executed defect, not just a hypothetical: 502/503/504 produced three POST /_reindex requests even after request-local MaxRetries(0). The pinned transport ignores that local value. Single-node dispatch is required; it preserves configured node predicates/forced-node settings and leaves ordinary client retries unchanged.
  • fix(reindex): stop a reindex from silently dropping documents #327 current starting point b458dd81d75095428c4d438ed93a32a707739cd9 includes newer checkpoint/routing/script/durable-state repairs. Additional task404/launch and retired-source tests are applied against that current tree, not the obsolete 091b2ba0 candidate. Run https://github.com/FoundatioFx/Foundatio.Repositories/actions/runs/35805762856 has demonstrated five task/launch regressions red then green; the live retirement stage is being validated separately. No new fix(reindex): stop a reindex from silently dropping documents #327 product head has been promoted yet.

Ordered work and remaining gates

  1. Lease ownership: integrate a released version containing #573, verify all migration call paths stop destructive mutations after lost ownership, and validate heartbeat lifetime. Add explicit Elasticsearch index compatibility upgrades #307's candidate supervises renewal independently of callbacks and forbids reset/unblock after observed loss. A checked renewal is not a server-side generation fence.
  2. Asynchronous launch and recovery authority: one effective dispatch, no task404/partial-evidence cleanup permission, persistent task lineage, no blind outer/proxy retry. Add explicit Elasticsearch index compatibility upgrades #307 single-dispatch is validated; fix(reindex): stop a reindex from silently dropping documents #327 durable task404/launch changes are in current regression validation. 9.5 relocation-specific cancellation is not represented as implemented for every supported server.
  3. Destination durability (Add explicit Elasticsearch index compatibility upgrades #307): target-only configured replica readiness before source deletion; configurable bounded wait. Candidate enforces green/no unassigned shards with renewable polling; normal CI must provision replicas rather than use its old one-node setup.
  4. Script/deletion reconciliation (fix(reindex): stop a reindex from silently dropping documents #327): verify the newer clean-and-rebuild output protocol, hard deletes/no-op remnants, and unsupported ranges/transformations. Do not infer correctness from destination >= source.
  5. Complete/routed transport evidence (fix(reindex): stop a reindex from silently dropping documents #327): retain and test all-item/shard/scroll/bulk/mget validation and routing+ID identity. An item error is not absence.
  6. Complete source-change evidence (fix(reindex): stop a reindex from silently dropping documents #327): retain and test UUID-bound complete primary checkpoint vectors, all changed-ID pages, and initially empty sources. A checkpoint snapshot is not a write barrier.
  7. Cutover / retirement / failback: retain the old physical source block after durable cutover intent, including lost responses and failing observers; never automatically reopen a cutover record. Proven pre-cutover cleanup and operator blocks remain distinct. New live regressions target this on fix(reindex): stop a reindex from silently dropping documents #327.
  8. Generation-bound recovery and operator contract: full persisted generation/attempt/configuration/alias manifest and enforceable takeover fence remain a separate protocol gate. Maintenance admission control, private destinations, stable metadata, snapshot validation, retry ownership, and concurrency-token invalidation remain mandatory. No automatic quarantine/failback after ambiguous cutover is being claimed.
  9. Integration/release verification: promote only clean reviewed candidates, reconcile actual fix(reindex): stop a reindex from silently dropping documents #327/Add explicit Elasticsearch index compatibility upgrades #307 tips, rerun the intended release tree and persistent chain/package checks, and replace historical/pending PR notes with observed results. Both PRs still target main; no force push or merge to main.

Architecture retained: strict offline compatibility replacement stays write-block-first. Low-downtime mutable migration requires a separate durable ordered change stream covering updates/deletes/version fencing. Slicing, throttling, and historical-partition migration do not silently supply that protocol. No production cluster or package-feed configuration is changed as a shortcut; the planned CI-only replica profile leaves the development compose file and publishing configuration intact.

Copy link
Copy Markdown
Member Author

Sequential step 2: new regression proves request-local retry setting is insufficient

Run https://github.com/FoundatioFx/Foundatio.Repositories/actions/runs/35802285678 built the original and proposed code cleanly. All three new transport regressions (502/503/504, three-node pool, global retry limit 2) observed three POST /_reindex submissions, both before and after adding request-local .MaxRetries(0). The five existing task-404 cases still passed. No candidate was promoted.

The transport binding implementation computes MaxRetries from the global configuration and pool, ignoring the request-local MaxRetries value; a non-null ForceNode sets the effective limit to zero: https://github.com/elastic/elastic-transport-net/blob/78dc23fe779254d1a06ef52bef44e3941c6fb049/src/Elastic.Transport/Components/Pipeline/BoundConfiguration.cs . This turns the earlier conditional concern into an executed regression and explains why merely setting MaxRetries(0) is not adequate with this dependency.

I am correcting the launch to use a single selected node for this one ambiguous side-effecting dispatch, without disabling retries for ordinary reads or changing the shared client globally. The regression gate remains unchanged and must pass before commit promotion. Persisted task intent/404 handling does not account for a duplicate launch whose task ID was never returned.

Copy link
Copy Markdown
Member Author

Sequential consistency remediation — release blocking

I am treating the following as ordered production gates and will close them one at a time with regression evidence before moving to the next:

  1. Lease ownership / fencing — a stale controller must stop mutating after lock ownership is lost. Repositories currently consumes Foundatio 13.0.4, whose cache-lock renewal path does not reliably surface a failed compare-and-replace.
  2. Async reindex single-dispatch / complete task lineage — an ambiguous submission must not be blindly retried into multiple untracked writers. A task 404 is not termination evidence.
  3. Destination durability before destructive compatibility cutover — restoring the replica setting is not enough; do not delete the source until the target has the intended assigned shard redundancy.
  4. Recovery authority / stale-worker protection — unknown or delayed requests must not be able to authorize cleanup, unblock, promote, or reuse a target after ownership changed.
  5. Cross-PR integration — after fix(reindex): stop a reindex from silently dropping documents #327 lands, reconcile its normal-schema migration protocol with Add explicit Elasticsearch index compatibility upgrades #307's compatibility protocol and rerun the actual combined tree.

Each item stays open until the implementation, red/green regression, documentation, and current-head validation agree. I will not mark #307 production-ready merely because the ordinary suite is green.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b6a7ec7996

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +108 to +110
catch (OperationCanceledException)
{
throw;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Roll back source-only cancellation before target creation

When the progress callback throws OperationCanceledException or returns a canceled task for the 5% notification, this branch propagates after the source marker and write block have been acknowledged but before CreateTargetAsync is called. Because canResetCurrentAttempt is still false and the source-only topology is classified as ManualIntervention, the source remains write-blocked and the public recovery API cannot clear it. Fresh evidence after the callback-failure fix is that the explicit cancellation branch still bypasses the exception swallowing; mark this locally proven pre-dispatch state as resettable and remove the marker/block before propagating cancellation.

Useful? React with 👍 / 👎.

@sunnyrp sunnyrp left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #307 — Elasticsearch index compatibility upgrades

Verdict: REQUEST CHANGES

Blockers: (1) CI build is red at head b6a7ec79. (2) Two unanswered P1/P2 threads are confirmed at head: sibling-alias deletion → data loss, and a stranded source write block. (3) Deterministic _reindex start rejections leave the source write-blocked with no public recovery path.
Warnings: stale seq_no/primary_term optimistic-concurrency tokens after cutover; final_pipeline can mutate copied docs undetected; no-progress timeout ignores the injected TimeProvider; PR size/scope; CLA pending.

Security | Build | Dependencies | Requirements | Code Review | Data-loss matrix | Pre-merge plan

Security: PASS

No malicious scripts, credential access, or obfuscation found. CodeQL passes. The workflow-permission alert was addressed in 5451d7f7.

Build: FAIL | CI: build / build failed on both runs (35809708683, 35809712324) with 1,170 tests, 1 failed

IndexCompatibilityTests.RunCompatibilityReindexAsync_TransientSubmissionIsNotRetriedAcrossNodes fails at IndexCompatibilityTests.Task.cs:109. Expected ["POST /_reindex"], got HEAD / first.

  • Root cause: StaticNodePool pings a node (HEAD /) before first use, and ForceNode does not disable that ping. The stub queued for POST /_reindex is consumed by the ping instead (see inline).
  • The PR description's "final-head CI" gate is therefore not met. The listed evidence runs target 9c681f8c, which is two commits behind the current head.

Dependencies: N/A for audit

One System.IO.Hashing reference was added. It is used only for the X-Opaque-Id correlation hash.

Requirements: GAPS

  • Stated scope is met in design: offline, operator-invoked; zero compatibility requests in ConfigureIndexesAsync; fail-closed recovery.
  • Remaining gates in the PR body are not yet closed at the current head:
    • full-solution ES 8.19/9.5 matrix
    • persistent 7→8→9 chain (the ES9 leg has not executed yet)
    • Windows packaged consumer
    • combined #327/#307/#308 validation

Code Review

# Sev Finding Impact
1 BLOCKER Wrapped-name branch of ValidateCompatibilityDeleteTarget (Index.cs:485-492) never calls IsNativelyClaimedByOtherConfiguredIndex(canonicalName) events.DeleteAsync() can delete events-v2's reindexed-v9-events-v2 → data loss (confirms open P1 thread)
2 BLOCKER Source is marked and write-blocked (Upgrader.cs:130-131). Any failure before CreateTargetAsync leaves canResetCurrentAttempt == false: refresh, lock renew, or callback cancellation at 5%. Recovery then classifies source-only as ManualIntervention Production source stays write-blocked with no public reset API (confirms/extends open P2 thread)
3 BLOCKER _reindex start: every non-success response (e.g. deterministic 400/403/404) and every pre-dispatch exception (ping, DNS, cancellation) becomes ElasticReindexTaskUncertainException (Runner.cs:90-101) Source write-blocked plus a marked, empty target, classified ManualIntervention; the operator must hand-edit cluster state
4 WARNING _reindex resets _seq_no/_primary_term. Foundatio versioned documents carry these as their concurrency token Clients holding pre-upgrade versions can hit spurious conflicts, or in rare collisions a lost update (inconsistency). Document it, and advise invalidating cached versions
5 WARNING Pipeline("_none") bypasses only default_pipeline. A copied index.final_pipeline still runs during _reindex Document bodies can be transformed while counts and mappings still match. Silent data inconsistency; reject or strip final_pipeline for the copy, then restore it
6 NIT No-progress timeout uses Stopwatch.StartNew() while delays use the injected TimeProvider (Runner.cs:174) The timeout branch cannot be tested with FakeTimeProvider; clocks are inconsistent
7 WARNING PR size/shape: +9.8k lines, 90+ commits, many fix-on-fix commits, 98 review threads Hard to review for regressions in the shared ElasticReindexer path (+297/−248) used by every consumer. Squash-merge, and consider splitting the reindexer refactor

Data-loss / inconsistency matrix

  • Source deletion is irreversible. It happens atomically inside the _aliases cutover (remove_index, Upgrader.cs:203-206). Count and mapping checks precede it, but there is no snapshot precondition. Require a verified snapshot in the runbook.
  • Sibling families (#1): physical index of another registered index deleted.
  • Stranded write blocks (#2, #3): writers fail until manual intervention. Not data loss, but an outage.
  • Concurrency tokens (#4) and final pipeline (#5): silent content or version divergence.
  • Cluster-wide readiness: as the PR notes, system indexes such as .geoip_databases can still block ES9 startup. Foundatio preflight ≠ Upgrade Assistant.

Existing comments

  • Outstanding (confirmed at head): 4075643315 (P1 sibling canonical alias), 4078470116 (P2 5% cancellation). Replied to both with verified details.
  • Earlier threads appear addressed by author replies. Not re-verified individually.

Recommended pre-merge plan

  1. Fix the CI test by adding .DisablePing(), or stub HEAD /. Decide whether product code should classify pre-dispatch failures as not started.
  2. Fix #1 by applying sibling arbitration in the wrapped branch, with a regression test for events + events-v2 + reindexed-v9-events-v2.
  3. Fix #2 and #3 by tracking "no create/reindex dispatched" locally, and resetting the source marker and block on those paths. Treat deterministic 4xx _reindex rejections as not started, so the reset path can clean up the empty target.
  4. Address or document #4 and #5 in docs/guide/index-management.md: pre-flight checks for final_pipeline, and a version-token caveat.
  5. Re-run and link, at the final head:
    • ordinary CI
    • ES 8.19.15 + 9.5.0 full matrix
    • standalone format check
    • Windows and macOS packaged consumer
    • persistent 7.17→8.19→9.5 chain with the ES9 leg actually executing
  6. Validate the combined #327/#307/#308 stack, or merge the others first and rebase.
  7. CLA: license/cla is pending.
  8. Squash-merge with a curated message; release as preview first; keep the audit branch unmerged.
  9. Operator runbook: snapshot → stop writers/index managers → Inspect → Upgrade → verify → Recover if needed, including rollback via snapshot restore.

new Uri("http://node-2.invalid:9200"),
new Uri("http://node-3.invalid:9200")
]), invoker)
.MaximumRetries(2);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCKER (CI red at head), 95% confidence. StaticNodePool enables pinging, so the transport sends HEAD / to node-1 before the forced POST /_reindex. The ping consumes the only stub, and the Request: "POST /_reindex" assertion in SequenceRequestInvoker fails (CI: Expected "POST /_reindex", Actual "HEAD /").

Minimal fix: add .DisablePing() here, or queue a HEAD / 200 stub first. Keep MaximumRetries(2) so the test still proves ForceNode prevents failover.

}
catch (Exception ex)
{
throw CreateUncertainStartException(sourceIndex, targetIndex, ex);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCKER (fail-closed, but unrecoverable), 85% confidence. Every exception here becomes ElasticReindexTaskUncertainException. That includes failures that provably happen before dispatch: the node ping above, DNS, OperationCanceledException before send, and SelectSingleDispatchNode failure. Lines 95-101 do the same for deterministic 4xx rejections such as 400 action_request_validation_exception or 403.

At this point the source is marked and write-blocked, and the target exists with its marker. onTaskTerminated never fires, so canResetCurrentAttempt stays false. GetRecoveryAction then returns ManualIntervention. Result: the production source stays write-blocked, and no public API can clear it.

Suggest classifying "no request bytes sent" and 4xx (excluding 408/429) as not started, and letting the caller reset. Keep 5xx/timeout/lost-response as uncertain.

int statusFailures = 0;
long lastProgress = 0;
var noProgressTimeout = ElasticReindexer.GetNoProgressTimeout(workItem);
var noProgressStopwatch = Stopwatch.StartNew();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT, 90% confidence. The no-progress window uses the wall-clock Stopwatch, while every delay uses the injected _timeProvider. With a FakeTimeProvider the timeout branch can never fire, so it is untested. Use _timeProvider.GetTimestamp() / GetElapsedTime() for consistency.

if (batchSize.HasValue)
source.Size(batchSize.Value);
});
d.Dest(destination => destination.Index(targetIndex).OpType(OpType.Create).Pipeline("_none"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING (silent data inconsistency), 70% confidence; please confirm. Pipeline("_none") suppresses only the destination's index.default_pipeline. _create_from copies index.final_pipeline, which always runs and cannot be bypassed per request. A non-idempotent final pipeline (timestamps, enrich, set) would rewrite every copied _source. Counts and mappings would still match, so verification passes and the original is then deleted.

Suggest failing validation when the source has index.final_pipeline, or stripping it on the target for the copy and restoring it before cutover.


workflowAttempted = true;
await AddWorkflowMarkerAsync(sourceIndex, false, cancellationToken).AnyContext();
await AddWriteBlockAsync(sourceIndex, cancellationToken).AnyContext();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCKER (stranded write block), 90% confidence. This extends open thread r4078470116. After this block is acknowledged, canResetCurrentAttempt is false until line 138. Every failure in that window leaves the source write-blocked with only the source existing:

  • RefreshAsync failure or cancellation
  • reindexLock.RenewAsync() failure inside ReportProgressAsync(5)
  • callback OperationCanceledException
  • CreateTargetAsync rejected with a deterministic 4xx

GetRecoveryAction maps that state to ManualIntervention (Recovery.cs:298-304), and CanResetCurrentAttempt requires TargetExists, so nothing can clear the block.

Suggest a local targetCreateDispatched flag. When it is still false, the process has positive evidence that no create is in flight, so it can safely remove the block and marker before rethrowing.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants