Repository navigation
Conversation
…s index (LeanerCloud#408) Migration 000140 adds a generated az_config column, read verbatim from payload->'details'->>'az_config' (RDS single-az/multi-az, Azure SQL zone redundancy, '' otherwise), and two unique indexes that include it: the 11-column natural key plus az_config (the new upsert arbiter) and the 000120 source-account key plus az_config. The existing 11-column and 000120 source indexes are kept, so the current upsert keeps working and this can be applied before the store switch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tency key (LeanerCloud#408) Single-AZ and Multi-AZ RDS recommendations of the same class, engine, term and payment shared one rec ID and one identity key. Read az_config verbatim from Details (RecommendationRecord.DetailsAZConfig, pinned to the 000140 generated column by shared vectors) and add it to RecIdentityKey, the rec ID (appended only when set, so every other rec keeps its ID), the purchase idempotency key and describeRec. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nfig (LeanerCloud#408) Requires migration 000140. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oud#408) Migration 000141 drops the 11-column natural key and the 000120 source index, both superseded by the 000140 indexes that add az_config. While either exists, a collection batch carrying a Single-AZ and a Multi-AZ RDS recommendation of the same class fails as a whole. The rollback collapses az_config-only duplicates to the newest row and recreates both. Apply only after the 12-column upsert is live everywhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
This was referenced Oct 23, 2026
This branch has not been deployed
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
#806 and #792 already added the zonal EC2 AZ, platform and tenancy to the recommendations natural key, rec ID, identity key and idempotency key. What #408 still lists as missing is EC2
scopeand RDSaz_config.az_config(fixed here). Cost Explorer reports Single-AZ and Multi-AZ RDS reservations of the same class, engine, term and payment as two separate recommendations (parseRDSDetailsmapsDeploymentOptiontosingle-az/multi-az). They are different offerings, but both got the same natural key, rec ID and identity key. That breaks three things on main:UpsertRecommendationswrites the whole multi-provider batch in one statement. When the batch has both variants it fails withSQLSTATE 21000("ON CONFLICT DO UPDATE command cannot affect row a second time"), so the whole collection write is lost, not just one rec.loadStoredRecommendationIndexrefuses every purchase for that provider with "more than one row for identity key".scope(no change needed). The AWS parser derives scope from the AZ (resolveEC2Scope: zonal exactly when CE gives an AZ). The AZ is already in every identity, so two recs cannot differ only by scope. Azure collects Shared-scope recs only (properties/scope eq 'Shared') and GCP does not set scope. The purchase guardcomputeDetailMismatchstill compares scope as a backstop.findOfferingIDalready filters byAZConfigand refuses an empty one, and EC2 filters already use platform, tenancy, scope and AZ, so no provider change is needed.Fix (four commits, expand/contract like the #792/#806 chains)
feat(db)migration 000140 (expand). Adds:az_configcolumn,COALESCE(payload->'details'->>'az_config', ''), verbatim. The table rewrite backfills it, so no data migration is needed.recommendations_natural_key_az_tp_azc_idx: the 11 columns plusaz_config.recommendations_natural_key_source_azc_idx: 000120's source-account key plusaz_config. This is needed because any unique index withoutaz_configstill rejects the pair. See the feat(insurance): per-recommendation Archera quotes (blocked on an Archera API mapping) #799 note below.The old indexes stay, so the current upsert keeps working.
fix(purchase): newRecommendationRecord.DetailsAZConfig(), pinned to the generated column by shared vectors.az_confignow goes into:RecIdentityKey. This covers the pricing index, planstep and the ramp-baseline netting.|<az_config>only when set, so EC2, cache and other recs keep their IDs.purchaseIdempotencyKey.describeRec.fix(config): the upsertON CONFLICTtargets the 12-column index. This needs 000140.fix(db)migration 000141 (contract). Drops the 11-column index and 000120'srecommendations_natural_key_source_idx. Each has a strictly finer replacement from 000140, so no existing row can violate the new indexes. The down migration collapsesaz_config-only duplicates to the newest row (the table is a regenerable cache) and recreates both indexes exactly.Prod deploy order (auto_migrate=false)
database-migration.ymlfor 000140 only (stepscounted from the live prod version).ON CONFLICTneeds the index that 000141 drops, so never runsteps=0before the release.Dev auto-migrates at startup, so the window there is one deploy. If a maintainer prefers separate PRs as in #808/#810/#811, the four commits split cleanly.
Behavior changes to know about
az_configchange once, on the next collection. A stale browser tab gets the usual 409 ("not in the current recommendation set").az_config, so it gets a 409. This is the same rule fix(purchase): carry EC2 platform and tenancy in rec ID, identity key and idempotency key (#806) #817 introduced for EC2 platform/tenancy, and the web modal echoes the listed rec's Details. I did not re-audit external API clients beyond what fix(purchase): carry EC2 platform and tenancy in rec ID, identity key and idempotency key (#806) #817 recorded.purchase_executionsrows: the identity and idempotency keys are recomputed from each row's persisted Details on both sides, so in-flight rows still hash and match consistently. Rows from before fix(db): redact bound query arguments at every log level #453, which have no Details, readaz_configas"", the same as fix(purchase): carry EC2 platform and tenancy in rec ID, identity key and idempotency key (#806) #817.Interaction with the #799 source-account chain
Migration 000120 (#881) added
recommendations_natural_key_source_idxas a prerequisite and did not switch the upsert to it. That index lacksaz_config, so it would keep rejecting the Single-AZ/Multi-AZ pair. This PR replaces it withrecommendations_natural_key_source_azc_idx, the same key plusaz_config. When #799 later switches to a source-aware upsert, it should target that index. 000120's down-migration guard still works, because rolling back past 000141 recreates both old indexes first.Verification
All runs were on macOS with Go 1.26 and real Postgres 17 through testcontainers. This is local unit, mock and integration evidence on synthetic fixtures. I did no real-cloud or real-account verification: a live CE response with both RDS deployment options was not exercised. Tests ran with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/nullbecause this machine's default AWS profile is invalid.Fail before / pass after
go test -tags integration ./internal/config/ -run 'AZConfig(Batch|Concurrent)'on the main schema and store code, with the index precondition disabled, fails in every batch case withfailed to insert recommendations batch: ERROR: ON CONFLICT DO UPDATE command cannot affect row a second time (SQLSTATE 21000). It passes after the fix, six repeated runs.az_configremoved fromRecIdentityKeyandpurchaseIdempotencyKey(pre-fix behavior):TestPriceRecommendationsFromStore_PricesFromRequestedAZConfigfails withstored recommendations for aws contain more than one row for identity key ....TestPriceRecommendationsFromStore_RequestWithoutAZConfigRefusedForRDS,TestPurchaseIdempotencyKey_DiffersByAZConfigandTestRecIdentityKey_DiffersByAZConfigfail.TestConvertRecommendations_RDSAZConfigInIDshows both variants gettingaws||rds|us-east-1|db.r5.large|mysql|1|all-upfront, andTestRecommendationID_MalformedAZConfigErrorsfails. Both pass after the fix.Migrations (
go test -tags integration ./internal/database/postgres/migrations/ -run 'TestMigration_(RecommendationsAZConfig|DropRecommendations11ColNaturalKey|RecommendationsSourceAccount|DropRecommendations9ColNaturalKey|RecommendationsPlatformTenancy)$'): all pass. They cover:Concurrency.
TestPostgresStore_UpsertRecommendations_AZConfigConcurrentruns 8 concurrent collections re-upserting the stored pair. All succeed and exactly one row peraz_configremains.Suites and checks
go build ./...go veton the touched packages, with and without-tags integrationgo test ./internal/mocks/ ./internal/config/ ./internal/scheduler/ ./internal/api/ ./internal/purchase/ ./internal/planstep/ ./internal/database/... -count=1go test -tags integrationfor./internal/config/,./internal/scheduler/,./internal/api/,./internal/purchase/and./internal/database/postgres/migrations/. One test needed a fixture bump:TestApproveAzurePricingRequiresMigrationAndRecollectionnow migrates to 140, because the upsert needs the new column.golangci-lintv2.10.1 with--allow-parallel-runners, with and without--build-tags integration: 0 issuesgocyclo -over 10on the changed non-test files: silentscripts/gosec-hook.sh: cleangit secrets --scanin an isolated temp repo with the changed files tracked: clean. A planted fake key was flagged.scripts/test-git-secrets-allowlist.sh: 137 PASSgit diff --check: cleanOut of scope / noted
23505on that index. Postgres only routes conflicts on the arbiter index toDO UPDATE. I reproduced this on unmodifiedorigin/mainwith 8 concurrent single-row upserts: it failed 1 run in 4 onrecommendations_natural_key_source_idx. This PR keeps an equivalent index (..._source_azc_idx), so the race is unchanged. The concurrency test seeds the rows first so that it covers the steady-state re-collection path. The collection single-flight marker makes this rare. It could be fixed by making the source index non-unique until feat(insurance): per-recommendation Archera quotes (blocked on an Archera API mapping) #799 switches the arbiter, which is the owner's decision.TestMigration_ExecutionsDetachKeepsUpdatedAt(000128, purchase_executions) failed once during a full migrations-suite run and passed 3/3 in isolation. It is time-based and unrelated to this change.az_config, which is display-only.Frontend follow-up for the UI owner
cellKey(frontend/src/recommendations.ts) groups recs by physical cell. It includes the AZ and the platform/tenancy pair, but notaz_config. Single-AZ and Multi-AZ RDS rows of the same class therefore render as variants of one cell. Purchases are still correct, becausesamePurchaseVariantIdentityalready comparesaz_configfor RDS, but they should be separate cells, the same way fix(frontend): include platform and tenancy in EC2 recommendation cell key #818 did it for EC2 platform/tenancy.Closes #408
Maintainer: please mirror the issue's triage labels on this PR:
priority/p2,type/chore,triaged,severity/low,urgency/this-quarter,impact/few,effort/m.🤖 Generated with Claude Code