fix(opensearch): ledger _mget must not resolve its index by CLR-type inference - #121
Merged
bfarmer67 merged 1 commit intoAug 26, 2026
Merged
Conversation
…inference
IntersectWithAppliedAsync issues one _mget, which carries an index in the
URL and in each body entry. The URL one was set explicitly from
OpenSearchMigrationOptions.LedgerIndex; the body entries were left to
resolve via IndexName.From<OpenSearchMigrationRecord>(), which reads
ConnectionSettings.DefaultMappingFor<T>() / DefaultIndex().
Neither AddOpenSearchClient nor AddOpenSearchAwsClient configures either,
so request serialization threw
Index name is null for the given type and no default index is set
before any byte reached the wire. MigrationRunner.RunAsync calls
IntersectWithAppliedAsync unconditionally whenever at least one migration
is discovered, so this broke every OpenSearch run on 3.0.0 and 3.1.0 --
including through the library's own runner and CLI. Not AWS-specific.
Set the ledger index per operation. No API change, no consumer action.
Adds the wire-shape test tier that was missing: real client, real
serializer, faked socket (InMemoryConnection). The existing provider unit
tests substitute IOpenSearchClient, which never serializes, and the
container-backed tests are compile-gated behind #if INTEGRATIONS plus
[TestCategory("LocalOnly")] and excluded from the CI matrix -- so nothing
in the suite could see this class of defect. A generalized probe asserts
that no ledger operation depends on type->index inference, guarding the
next regression rather than only this one.
Two integration tests cover IntersectWithAppliedAsync against a real
cluster; one drives the client built by services.AddOpenSearchClient(...),
which nothing previously exercised end-to-end.
Records the invariant as ADR-0029.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bfarmer67
force-pushed
the
devs/bfarmer/opensearch-ledger-mget-index-inference
branch
from
August 26, 2026 17:31
f0517eb to
65e473a
Compare
bfarmer67
deleted the
devs/bfarmer/opensearch-ledger-mget-index-inference
branch
August 29, 2026 03:40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
IntersectWithAppliedAsyncissues a single_mget. An_mgetcarries an index in two places — the URL, and each entry in the request body. We set the URL one explicitly fromOpenSearchMigrationOptions.LedgerIndexand left the body entries to default, which resolves viaIndexName.From<OpenSearchMigrationRecord>()→ConnectionSettings.DefaultMappingFor<T>()/DefaultIndex().Neither
AddOpenSearchClientnorAddOpenSearchAwsClientconfigures either. So request serialization threw before a byte reached the wire:MigrationRunner.RunAsynccallsIntersectWithAppliedAsyncunconditionally whenever at least one migration is discovered. This broke every OpenSearch run on 3.0.0 and 3.1.0, including through our ownHyperbee.MigrationRunner.OpenSearchand the CLI (StartupExtensionscallsAddOpenSearchClient(config)).Two things worth flagging about scope:
ConnectionSettings. The.Awspackage is where it happened to surface first, not where it lives..Index(...)explicitly. The generalized probe test below confirms that.The fix
No API change, no consumer action, patch-level. Consumers who worked around this with their own
DefaultMappingFor<OpenSearchMigrationRecord>or a forked client factory can delete both.I deliberately did not fix this by configuring
DefaultMappingForinsideAddOpenSearchClient. The ledger index is runtime configuration onOpenSearchMigrationOptions, which the client factory cannot see —AddOpenSearchClientruns independently of, and usually before,AddOpenSearchMigrations. That route creates a registration-order dependency and still fails for anyone registering their ownIOpenSearchClient. Rationale recorded in ADR-0029.Why the suite missed it
Four gaps had to line up:
OpenSearchRecordStoreTestssubstitutesIOpenSearchClient. A substitute never serializes, so no inference failure is reachable. That file also only covers ctor lock-tuning —IntersectWithAppliedAsynchad zero unit coverage.RecordStoreContractTests/ReconciliationTestssubstituteIMigrationRecordStore— they pin interface semantics, not wire behavior.OpenSearchTestContainerhand-rolls its ownConnectionSettings;AddOpenSearchClientwas tested only for registration guards (AWS-endpoint rejection, mutual exclusion), never for whether the client it returns can run a ledger operation.#if INTEGRATIONS-gated, all[TestCategory("LocalOnly")], and the matrix is off (if: vars.RUN_HEAVY_INTEGRATION == 'true', disabled in 7ff3808, which landed immediately before 3.1.0). Release runs unit tests only. Even enabled,OpenSearchRecordStoreIntegrationTestsnever called the method.The structural miss: nothing existed between "mock the client" and "spin up Docker." That is precisely where serialization and inference bugs live.
Tests
New tier —
OpenSearchLedgerWireTests. Real client, real descriptors, real serializer, over anInMemoryConnection. Only the socket is faked. Runs in ~60ms with no container, so it runs in the CI tier that actually executes.Integration. Two tests for
IntersectWithAppliedAsync— one against a real cluster with realtime (no-refresh) semantics, one driving the client fromservices.AddOpenSearchClient(...), which closes gap 3.Verified red → green:
OpenSearchLedgerWireTests(5)OpenSearchRecordStoreIntegrationTests(8, real container)Follow-ups
Two related PRs follow, both building on ADR-0029:
IntersectWithSquashedAsyncmixes a typedKindterm with a literal"replaces"term — the filter can never match, so squash reconciliation silently returns empty and squashed migrations re-run. Live today. Plus the latent Couchbase equivalent.ConnectionSettingsescape hatch on both client factories. Worth having on its own merits, but not the remedy for this bug.🤖 Generated with Claude Code