Skip to content

fix(gcp): price tiered SKUs from the first paid tier - #433

Open
IAmJSD wants to merge 1 commit into
LeanerCloud:mainfrom
IAmJSD:fix/issue-242
Open

IAmJSD wants to merge 1 commit into
LeanerCloud:mainfrom
IAmJSD:fix/issue-242

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

Root cause

extractSQLPriceFromSKU (Cloud SQL), extractPriceFromSKU (Memorystore) and extractStoragePriceFromSKU (Cloud Storage) priced a SKU from TieredRates[0] only. A SKU whose first tier is a free allowance ([0, >0]) came back as price 0, and the price == 0 → continue guard in each extract*PricingFromSKUs loop then dropped it, so the region/tier got no price instead of the paid one.

Fix

  • New skumatch.PaidRate(*cloudbilling.PricingExpression) *cloudbilling.TierRate returns the first tier with a non-zero unit price (skipping nil tiers and tiers with no UnitPrice), or nil when every tier is free.
  • The three extractors call it in place of TieredRates[0]. Everything after that (nanos math, Cloud Storage unit conversion, currency, slot) is unchanged.
  • I picked "price from the first non-zero tier" (the first option in the issue's acceptance criteria). The catalog lists tiers by ascending StartUsageAmount, and a workload large enough to need a commitment is past the free allowance, so the first paid tier is the rate it pays at the margin. This is a documented assumption. I did not check the live Billing Catalog for a real Cloud SQL or Memorystore SKU shaped like this, and I made no GCP API calls.
  • SKUs where every tier is free are still skipped, as before. A negative price is still non-zero, so the existing "negative catalog price" tests behave as before.

Closes #242

Verification

All of this is unit/fixture evidence built from hand-written cloudbilling.Sku values. No real GCP catalog data was used.

Fail-before (new tests run on main's extractors, before the fix):

--- PASS: TestPaidRate
--- FAIL: TestExtractSQLPricing_FreeFirstTierUsesPaidTier
--- PASS: TestExtractSQLPricing_AllTiersFreeIsSkipped
--- FAIL: TestExtractPricing_FreeFirstTierUsesPaidTier            (memorystore)
--- PASS: TestExtractPricing_AllTiersFreeIsSkipped                (memorystore)
--- FAIL: TestExtractStoragePricing_FreeFirstTierUsesPaidTier
      Max difference between 2.7397260273972603e-05 and 0 ...      (price was 0)
--- PASS: TestExtractStoragePricing_AllTiersFreeIsSkipped

(TestPaidRate tests the new helper, and the AllTiersFree tests are guards that pass before and after the fix.)

Pass-after, run from providers/gcp:

AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null go test ./...                  # all ok
GOWORK=off AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null go test -race -short ./...   # all ok
go build ./... && make vet                                                                       # clean
golangci-lint v2.10.1 run --allow-parallel-runners ./...                                         # 0 issues
gocyclo -over 10 internal/skumatch/skumatch.go services/{cloudsql,memorystore,cloudstorage}/client.go  # silent

I set AWS_CONFIG_FILE/AWS_SHARED_CREDENTIALS_FILE to /dev/null only because the local machine's default AWS profile is invalid. The GCP tests don't depend on it.

Mutation check: if PaidRate goes back to returning TieredRates[0], the three FreeFirstTierUsesPaidTier tests and the free first tier then paid cases in TestPaidRate fail.

Interaction with #424 (fix/issue-243)

#424 also edits providers/gcp/services/cloudstorage/client.go, in extractStoragePricingFromSKUs currency handling and the import block. This PR changes only the TieredRates[0] lines in extractStoragePriceFromSKU, adds no import there (skumatch is already imported), and puts its test in a new tiered_rates_test.go instead of #424's pricing_currency_test.go. A trial merge of fork/fix/issue-243 into this branch auto-merged with no conflicts, and go build ./... plus the cloudstorage/skumatch tests passed on the merged tree.

Out of scope

  • providers/gcp/services/computeengine/pricing.go:259 also reads only TieredRates[0]. The issue doesn't list it, so I left it alone. It could switch to skumatch.PaidRate in a follow-up.
  • Not checked against the real catalog: whether any Cloud SQL / Memorystore / Cloud Storage SKU actually has a [0, >0] tier shape.

Maintainer: please copy the issue's triage labels onto this PR: triaged, priority/p3, severity/low, urgency/eventually, impact/few, effort/s, type/bug.

🤖 Generated with Claude Code

The Cloud SQL, Memorystore and Cloud Storage extractors read only
TieredRates[0], so a SKU whose first tier is a free allowance and whose
later tier is paid came back as price 0 and was skipped entirely.

Add skumatch.PaidRate, which returns the first tier with a non-zero unit
price, and use it in all three extractors. SKUs whose tiers are all free
are still skipped.

Closes LeanerCloud#242

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 8 billable files and costs up to $2.00.

Or wait 41 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-go/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 511e679e-a4db-4e76-9743-2766ef55dfbd
📥 Commits

Reviewing files that changed from the base of the PR and between 740dfbd and 48dd697.

📒 Files selected for processing: 8
  • providers/gcp/internal/skumatch/paid_rate_test.go
  • providers/gcp/internal/skumatch/skumatch.go
  • providers/gcp/services/cloudsql/client.go
  • providers/gcp/services/cloudsql/tiered_rates_test.go
  • providers/gcp/services/cloudstorage/client.go
  • providers/gcp/services/cloudstorage/tiered_rates_test.go
  • providers/gcp/services/memorystore/client.go
  • providers/gcp/services/memorystore/tiered_rates_test.go
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@cristim cristim added priority/p3 Polish / idea / may never ship severity/low Minor harm urgency/eventually No deadline impact/few Limited audience effort/s Hours type/bug Defect triaged Item has been triaged labels Oct 16, 2026
@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member

Independent adversarial review: CLEAN at 48dd6971 (claude-go-pr-merge-2, non-author reviewer). No blocking findings.

  • PaidRate (skumatch.go:101-117) treats a tier as paid when Units or Nanos is non-zero (nanos-only prices count); all-free, nil expression, nil rate and nil UnitPrice return nil, and all three callers keep the previous "no price" handling. Price math Units + Nanos/1e9 and the tier's currency code are correct for Money (e.g. cloudsql/client.go:467-468).
  • Fail-before: with origin/main's client.go for cloudsql, cloudstorage and memorystore, TestExtractSQLPricing_FreeFirstTierUsesPaidTier, TestExtractStoragePricing_FreeFirstTierUsesPaidTier and TestExtractPricing_FreeFirstTierUsesPaidTier fail; they pass at this head.
  • Mutations: dropping the Nanos check fails TestPaidRate; iterating TieredRates[:0] fails TestPaidRate and cloudsql tests.
  • Non-blocking: (1) PaidRate assumes tiers arrive sorted by StartUsageAmount; the API type (cloudbilling-gen.go:1232) does not promise order. Sorting before the scan would be cheap hardening; no known SKU violates it. (2) computeengine/pricing.go:253-265 still reads TieredRates[0] but fails loud on a zero price, and fix(gcp): pricing extractors read only TieredRates[0], so a SKU whose first tier is free is skipped #242 scopes only the three extractors changed here.
  • Local verification (macOS, unit/mock, no GCP calls): (cd providers/gcp && go test -race -short ./internal/skumatch/... ./services/...) pass; make build, make vet pass.

Still needed before merge: owner approval of the fork CI runs and green CI on this SHA, and the author marking the PR ready. Labels mirrored from #242. CR skipped: quota/draft; the merge comment will carry CR waived: quota, adversarial review + local verification + green CI once CI is green.

@IAmJSD
IAmJSD marked this pull request as ready for review October 16, 2026 10:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/few Limited audience priority/p3 Polish / idea / may never ship severity/low Minor harm triaged Item has been triaged type/bug Defect urgency/eventually No deadline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(gcp): pricing extractors read only TieredRates[0], so a SKU whose first tier is free is skipped

2 participants