Skip to content

test(purchase): pin the Azure savings-plans alias to the re-drive token through the real client - #1305

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

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

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

What #838 asked, and what the pinned module actually does now

#838's matrix was written against cloud-commitments-go ad67030387e4. Its open gap was that the Azure savings-plans client names its order alias from time.Now().UnixNano(), so the platform's DeriveIdempotencyToken(lineage, index) never reaches Azure.

I re-checked that against the module go.mod pins now (providers/azure v0.0.0-20261017134002-3def3f898617). That row is out of date. services/savingsplans/client.go:376 now does:

aliasName, err := common.OrderIDFromToken(opts.IdempotencyToken, fmt.Sprintf("cudly-%d", time.Now().UnixNano()))

So with a token (the platform's purchase path), the alias is the deterministic GUID derived from the token. The timestamp name is used only with an empty token (the CLI path), and a malformed token fails before any PUT. The module tests this with a mock in TestPurchaseCommitment_DeterministicAliasNameFromToken.

The gap in the platform: nothing here pinned it. TestRedriveReusesDerivedTokensForEveryProvider stops at a mock service client and does not cover azure savings-plans at all. A go.mod bump that brought the timestamp alias back would pass every platform test.

Change (tests only, no production change)

New file: internal/purchase/azure_savings_plans_alias_proof_test.go, with TestAzureSavingsPlansAliasIsDerivedFromTheRedriveToken. It runs processPurchaseRecommendations (the platform's real per-rec fan-out and token derivation) against the real pinned savingsplans.Client. Only the Azure order-alias API (SetOrderAliasClient) is faked. For both dispatchable slugs (savings-plans and savingsplans) it asserts:

  • each recommendation PUTs OrderIDFromToken(DeriveIdempotencyToken(lineage, index));
  • a re-drive of the same lineage PUTs the identical alias names;
  • recommendations and lineages do not collide, so a genuinely new purchase is not swallowed by an earlier alias;
  • no alias is the cudly-<nanos> timestamp fallback;
  • the provider is asked for common.ServiceSavingsPlansAll, the only service type Azure routes to this client;
  • recRedriveRefusalReason still refuses Azure savings-plans re-drive (see below).

Why the platform keeps refusing Azure savings-plans re-drive

A deterministic alias is necessary but not sufficient. Whether Azure answers a second PUT savingsPlanOrderAliases/{name} by returning the first order is server behavior. That could mean neither a second plan nor an error, and no fake can prove it. Neither repo has a test or a cited provider doc for it. So this PR keeps option (a) from the issue: refuse automatic re-drive and Retry for Azure savings plans. That covers allRecsSafeToRedrive, the #1668 Retry gate and the #1718 armedRedriveRefusal backstop, all of which go through recRedriveRefusalReason. The new test fails if someone relaxes that as a side effect.

Remaining exposure, unchanged: a first attempt whose response is lost. The row fails, and a human who then starts a new purchase gets a new lineage, so a new alias and a second plan. That is a legitimate new buy from the platform's point of view.

Proposed cloud-commitments-go change (not made here; go.mod untouched)

This would let the owner choose option (b) safely. In providers/azure/services/savingsplans/client.go PurchaseCommitment, when opts.IdempotencyToken != "", first call GET savingsPlanOrderAliases/{aliasName}. That is armbillingbenefits.SavingsPlanOrderAliasClient.Get, which exists in the pinned armbillingbenefits v1.0.0; add Get to the SavingsPlanOrderAliasAPI interface and realOrderAliasClient.

  • 200: adopt the existing order and return Success with its ID, with no PUT. This is the same shape as Azure reservations' DoIdempotentPurchaseTwoStep lookup.
  • 404: proceed to BeginCreate.
  • Any other error: fail closed, with no PUT.

Add module tests for each branch, including "existing alias, so BeginCreate is never called". After that lands and go.mod is bumped, the platform can drop the azure/savingsplans case in recRedriveRefusalReason. It would update the existing guard tests (TestRedriveGuardRefusalSetIsExactlyTheDispatchableSpellings, TestRedriveRefusedWhereNoProviderGuardExists) and the last assertion of the new test.

Matrix re-verified at the current pins

The pins are providers/aws aeb8a6f7ff51, providers/azure and providers/gcp 3def3f898617, and pkg 541e05e5fd29. I read the module source in the Go module cache.

Provider / service Token use at the pin Module test Platform re-drive
AWS Savings Plans CreateSavingsPlanInput.ClientToken (savingsplans/client.go:250) TestClient_PurchaseCommitment_SetsClientTokenForIdempotency allowed
AWS RDS, ElastiCache, MemoryDB, OpenSearch IdempotentReservationID + idempotencyGuard / recoverAlreadyExists (e.g. rds/client.go:150,173) rds *Idempotent* (5 tests), plus purchase_empty_token_test.go per service allowed
AWS EC2 tag lookup findRIByIdempotencyToken before buying (ec2/client.go:145,226) ec2/client_test.go refused
AWS Redshift corrected: not a tag lookup. Reserved nodes cannot be tagged, so guardAgainstRepurchase matches an owned node on offering ID, node count and start time (redshift/client.go:113-139) redshift/client_test.go *Idempotent* refused
Azure compute, cache, cosmosdb, database, managedredis, search, synapse reservations.DoIdempotentPurchaseTwoStep (internal/reservations/purchase.go:679) TestFindReservationOrderByIdempotencyToken_* etc. allowed
Azure Savings Plans corrected: alias = OrderIDFromToken(token) (savingsplans/client.go:376); server-side collapse of a repeat PUT unproven TestPurchaseCommitment_DeterministicAliasNameFromToken (mock); now also pinned here refused
GCP Compute Engine RequestId from OrderIDFromToken(token) + deterministic name (computeengine/client.go:829,873-878) TestComputeEngineClient_PurchaseCommitment_IdempotentReDrive allowed
GCP Cloud SQL, Cloud Storage, Memorystore PurchaseCommitment returns an error (no purchase API) n/a n/a (cannot buy)
any other provider n/a n/a refused

Owner decisions left open (why this is Refs, not Closes)

  1. Azure savings plans: keep (a), refusing permanently, or adopt the go change above and then relax. The platform is safe either way; this PR only pins the current state.
  2. EC2/Redshift: whether to relax the refusal. Redshift's guard is a spec match, not a token tag, and its comment requires that there be no concurrent same-spec purchase in the account and region. I would keep refusing Redshift. EC2's tag lookup is closer to safe, but the tag is written by a separate CreateTags after the buy, so a crash between the buy and the tag still leaves an untagged RI. That argues for keeping the refusal there too.

Deploy

None needed. This PR is tests only, and the platform behavior is unchanged. The Azure/GCP deploy pause since 2026-10-20 does not affect it.

Coordination with in-flight work

This PR adds one new test file and touches no production lines. Our fix/issue-243 branch already rewrites the stale "UnixNano" comments in internal/purchase/manager.go (the RedriveRefusalReason/recRedriveRefusalReason/safeFail doc comments), so I deliberately left them alone here to avoid a conflict. It does not overlap #1111, #1245 or fix/issue-532/-54/-361.

Refs #838

Verification

All of the following is unit, mock or fixture evidence. Nothing calls Azure or makes a purchase. The Azure order-alias API is a fake recorder.

Env: AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true, Go 1.26.9, Linux.

  • go build ./..., go vet ./..., go vet -tags integration ./...: OK
  • go test ./internal/purchase/ -count=1: ok (32.6s)
  • go test ./internal/mocks/ -count=1: ok
  • golangci-lint run --allow-parallel-runners ./internal/purchase/ (v2.10.1): 0 issues
  • gofmt -l internal/purchase/: clean. GOWORK=off go mod tidy -diff: clean (go.mod/go.sum unchanged; armbillingbenefits was already a direct require).
  • gocyclo: no non-test Go files changed.

Fail-before and pass-after, by mutation. The test pins existing behavior, so "before" means a regression of each link in the chain:

  • Module regression (a temp copy of the pinned azure module with line 376 changed back to the cudly-<UnixNano> alias, wired in through GOWORK=off go test -modfile=<tmp>/go.mod with a replace; the repo's go.mod is untouched): FAIL. Expected c505bf94-f62f-1682-6300-54342dcab999, got cudly-1792744476476032937; the re-drive alias differs; "alias … is the timestamp fallback". Both subtests fail.
  • Platform token regression (execution.go derives the token from exec.ExecutionID instead of idempotencyLineageKey(exec)): FAIL, "each purchase PUTs the alias derived from DeriveIdempotencyToken(lineage, index)".
  • Guard relaxed (recRedriveRefusalReason azure savings-plans case disabled): FAIL, "Azure savings-plans re-drive must stay refused until a repeat alias PUT is proven to dedupe".
  • Unmutated: go test ./internal/purchase/ -count=1 -run TestAzureSavingsPlansAliasIsDerivedFromTheRedriveToken -v: PASS (both subtests).

Combined-tree verification. This branch was tested together with the other queued branches on a local, never-pushed tree: queue/platform @ b56afb2b = origin/main a14dc8f2 plus these heads, merged in this order:

Branch Tested head
fix/issue-204 76f85a4
fix/issue-438 78fe011
fix/issue-838 120b07f
fix/issue-143 aef1256
fix/issue-349 cfbb551
fix/issue-80 63405f9
fix/issue-199 36b9f8f
fix/issue-41 96fc237
fix/issue-299 6af14ed
fix/issue-324 3ab6729

fix/issue-324 and fix/issue-204 both edit .github/workflows/ci.yml next to the azure-kv-access-policy job. The tree kept #324's new "Run Function URL reader self-tests" step and #204's updated job comment; whichever PR merges second takes that same resolution.

Linux, Go 1.26.9, AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true; Docker Postgres via testcontainers. The full suite ran on ed0c7174, which differs from b56afb2b only by one .gitallowed line on fix/issue-324:

  • Passed: go build ./..., go vet ./..., go vet -tags integration ./... and the full go test -race -short -count=1 ./....
  • Static checks: golangci-lint v2.10.1 found 0 issues on every changed package, with and without --build-tags integration. gocyclo -over 10 is silent on the changed non-test files.
  • Integration tests, every changed package that has them, one package at a time (-tags integration -count=1, 60-minute local timeout): internal/api, internal/purchase, internal/scheduler and internal/server all pass.
  • Secrets: the first run caught a bare placeholder account ID in fix/issue-324's new tftest, now allowlisted with an anchored .gitallowed line. Re-run at b56afb2b: git-secrets, using the repo's scripts/setup-git-secrets.sh patterns in a fresh repo with an isolated HOME, found nothing in the changed files, and a planted fake key was flagged. scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.
  • Nothing was applied to any cloud.

Out of scope

  • The go-side pre-PUT Get (above) belongs in cloud-commitments-go.
  • The CLI path (empty token, so the timestamp alias is still used) is cloud-commitments-cli's concern.
  • Any real-Azure check of repeat alias PUT semantics needs an owner-run sandbox test. It was not attempted.

Maintainer: please mirror #838's triage labels onto this PR: priority/p2, severity/medium, type/chore, triaged, urgency/this-quarter, impact/few, effort/m.

🤖 Generated with Claude Code

…en through the real client

The LeanerCloud#838 matrix (written against cloud-commitments-go ad67030387e4) says the
Azure savings-plans client names its order alias from time.Now().UnixNano(),
so the platform's derived idempotency token never reaches Azure. The module
pinned in go.mod now (providers/azure 3def3f898617) derives the alias from the
token via common.OrderIDFromToken, but nothing on the platform side pinned
that: TestRedriveReusesDerivedTokensForEveryProvider stops at a mock service
client and does not cover azure savings-plans.

Run the platform purchase fan-out against the real pinned savings-plans client
with only the order-alias API faked, and assert that each recommendation PUTs
OrderIDFromToken(DeriveIdempotencyToken(lineage, index)), that a re-drive PUTs
the identical names, that recommendations and lineages do not collide, and
that the platform still refuses Azure savings-plans re-drive (whether Azure
collapses a repeat alias PUT is server behavior no fake can prove).

Tests only; no production change.

Refs LeanerCloud#838

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD IAmJSD added priority/p2 Backlog-worthy severity/medium Moderate harm type/chore Maintenance / non-user-visible triaged Item has been triaged urgency/this-quarter Within the quarter impact/few Limited audience effort/m Days labels Oct 23, 2026
@coderabbitai

coderabbitai Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

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

effort/m Days impact/few Limited audience priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant