Repository navigation
fix(history): clear stale convertible stamps on AWS EC2 RI history rows - #1105
Conversation
Before LeanerCloud#509, savePurchaseHistory stamped every AWS 'ec2' purchase_history row 'convertible' regardless of global_config.offering_class, so Standard RIs bought while the setting was 'standard' are recorded as convertible and the marketplace-sell guard refuses them. global_config keeps no history, so only AWS can tell those rows apart. Migration 000133 implements option (a) from the issue: it sets offering_class to NULL on AWS 'ec2'/'compute' rows stamped convertible, so the marketplace-list handler's populateOfferingClass re-fetches the class from AWS and re-stamps the row. 'standard' stamps, other services and other providers are untouched; a 'standard' stamp committed while the migration waits on the row lock survives (READ COMMITTED re-check). The down migration is a no-op. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
📒 Files selected for processing: 3
✨ Finishing Touches
Comment |
|
Independent review (claude-cc-platform reviewer, cold brief): APPROVE No blocking findings. The PR is one commit: migration 000133 (up and down) plus an integration test. It implements option (a) from #524. Rows touched: AWS EC2 only ( Consumers:
So a genuinely Convertible, still-active RI shows Sell once, and its first click returns 400 and re-stamps it. The PR discloses this. Attacked:
Base drift: clean merge onto current main; the three files are new. Nits:
|
|
Independent review (claude-fleet): APPROVE 60bccb6 The migration is correct, idempotent and tightly scoped, and every probe was caught by a test. The one real risk is merge order, which nothing in the repo or CI checks. Findings (none blocking):
Migration order: main ends at 000130. Open PRs claim 000131 (#1092), 000132 (#1093), 000133 (#1105, unique), 000134 (#1110), 000135 (#1116), 000136 (#1140) and 000137 (#1170), with no duplicates. A lower number must deploy before any higher one, or it is silently skipped on databases that are already migrated. Nothing enforces this: CI's Attacked:
Verified locally, on a
Reviewed by an independent agent with no author context. Review only: nothing was pushed, and the GitHub review state is unchanged. |
|
Local verification (macOS, exact head
Gate status: two independent reviews approve this SHA. Merge waits on @IAmJSD marking the PR ready (see #974), and on migration order: 000131 (#1092) and 000132 (#1093) must merge first, because golang-migrate skips lower versions on an already-migrated database. |
|
Local verification at
|
|
Marked ready and merged by the owner's agent after 2 independent reviews and local verification (see the review and verification comments above, all at |
Root cause
Before #509,
savePurchaseHistoryhard-codedoffering_class = 'convertible'for every AWSec2purchase_history row, whateverglobal_config.offering_classsaid. Rows bought while the setting wasstandard(from 425bc2f until #509 was deployed) are Standard RIs recorded as convertible, soisKnownNonStandardOfferingClassreturns 400 on Sell and the History page hides the button. #509 fixed new writes but left the existing rows alone, andglobal_configkeeps no history, so the database alone cannot tell which convertible rows are wrong.Fix
Migration
000133_purchase_history_reverify_ec2_offering_classimplements option (a) from the issue:NULL is the column's existing "unknown" state. The History page already shows Sell for an empty class, and
marketplaceListcallspopulateOfferingClass, which fetches the class from AWS (DescribeReservedInstances) and re-stamps the row. That path already exists and is covered byTestMarketplaceList_EmptyOfferingClassFetchedStandard/...FetchedConvertible. No Go or frontend code changes.ec2and canonicalcompute). This also clears post-fix(purchase): stamp the purchased EC2 offering class on history #509 rows where an idempotent re-drive stamped the class configured at retry time, as the issue's "Also" section describes.LOWER(...)matches the sell guard's case-insensitive comparison.standardstamps, other AWS services and other providers are left alone.SELECT 1). It can't tell which rows it cleared, and re-stamping NULL EC2 rowsconvertiblewould block Standard RIs again.convertible, so the button goes away after that. If AWS can't find the RI (expired or exchanged), the fetch fails with a 400 and the row stays NULL.Concurrency: this is a single UPDATE inside the migration transaction. If the marketplace handler's
StampOfferingClasswritesstandardon a row while the migration is running, the migration waits on that row lock. Under READ COMMITTED it then re-checks the WHERE clause against the committed row and skips it, so the AWS-verifiedstandardsurvives.TestMigration133_ConcurrentStandardStampWinstests this on real Postgres: it holds the stamp transaction open, waits untilpg_stat_activityshows the migration blocked on the lock, commits, then assertsstandard. The opposite order (migration commits first, then the stamp) just leaves the stamp's value in place. Aconvertiblestamp landing at the same moment can be cleared and re-fetched later, which is harmless.Closes #524
Verification
All evidence below is local integration testing against a real Postgres 16 testcontainer, using fixture rows. No AWS calls were made.
Fail-before: I replaced the up file with
SELECT 1;.ClearsConvertibleEC2Stampsthen fails onri-ec2-convertible,ri-compute-convertibleandri-ec2-convertible-upper(each keepsconvertible).ConcurrentStandardStampWinsalso fails, because no statement waits on the lock.Mutation probes (each one makes the test fail):
LOWER()ri-ec2-convertible-upperprovider = 'aws'azure-compute-convertibleri-rds-convertibleservice IN ('ec2')onlyri-compute-convertibleri-ec2-standard,ri-compute-standard, and the race testA down migration that re-stamped cleared rows would fail the rollback assertions.
Other checks:
go test -tags integration -count=1 -p 1 ./internal/database/postgres/migrations/: ok (whole package, 135s)go test -count=1 ./internal/database/postgres/migrations/ ./internal/mocks/: ok (includes the transactional-statement guard and the repo-wide test guards)go build ./...,go vet ./internal/database/postgres/migrations/andgo vet -tags integration ...: cleangolangci-lintv2.10.1run --build-tags integration ./internal/database/postgres/migrations/: 0 issuesgocyclo -over 10 -ignore "_test\.go": silent (no non-test Go changed)check-migration-conflicts: no duplicate numbers.000133is the next free number: main ends at 000130, and open PRs feat(exchange): record an audit trail for direct-API RI exchange executes #1092 and fix(analytics): cut savings_snapshots partitions at UTC month starts #1093 use 000131 and 000132.scripts/gosec-hook.shon the new file: cleangit-secrets --scanin an isolated temp repo: cleanscripts/test-git-secrets-allowlist.sh: 42 PASS, 0 FAILAWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/nullbecause the default AWS profile on this machine is invalid. The migration doesn't use AWS.Coverage gap: this hasn't been run against a real deployment's data or real AWS RIs. The issue's acceptance check ("no AWS ec2 row disagrees with AWS") holds for
convertiblebecause none remain after the migration. Each row only becomes AWS-verified when someone attempts Sell on it.Out of scope
PurchaseResult. This migration clears any wrongconvertiblere-drive stamps that already exist, but new ones can still be written.standardthat is actually Convertible (a re-drive under astandardsetting).marketplaceListtrusts a storedstandardwithout asking AWS, and AWS would reject the listing. This is a separate issue, not part of this one.FetchOfferingClassjob. Option (a) needs no per-account credential plumbing and no AWS traffic at deploy time.Maintainer: please mirror the issue's triage labels onto this PR:
priority/p2,severity/medium,triaged,urgency/this-quarter,impact/few,effort/m,type/bug.🤖 Generated with Claude Code