Skip to content

fix(api): keep marketplace listing tracked when rollback cancel fails - #506

Merged
cristim merged 2 commits into
mainfrom
fix/marketplace-cancel-failure-state
Oct 5, 2026
Merged

cristim merged 2 commits into
mainfrom
fix/marketplace-cancel-failure-state

Conversation

@cristim

@cristim cristim commented Oct 5, 2026

Copy link
Copy Markdown
Member

What

reserveAndCreateListing (internal/api/handler_marketplace.go): when AWS created a listing, the DB write of that listing failed, and the compensating CancelMarketplaceListing also failed, the handler still released the claim (restoring the pre-listing fields) and returned "listing has been rolled back". The AWS listing stayed live, the row no longer referenced it, and the released claim allowed another listing to be created.

Fix

Only that branch changes (new helper keepUncanceledListing):

  • The claim is not released. The handler writes the known listing ID and AWS state (active) instead, using the existing UpdatePurchaseHistoryListing and listing-state constants. This is the same number of DB writes as before (it replaces the release write). With the listing recorded, marketplace-cancel can cancel it, and ClaimMarketplaceListingSlot blocks a duplicate listing.
  • If that write also fails because the DB is still down, the row keeps its pending claim. That claim blocks a duplicate listing, and the message tells the operator to reconcile the listing.
  • The response is a 502 ClientError that names the listing ID and the AWS cancel error. This follows the existing mapAWSMarketplaceError 502 pattern. The DB error text is logged and not returned. Previously this path returned a generic 500 that said nothing about rollback.

Unchanged: the path where compensation succeeds (cancel succeeds, claim released, "rolled back"), and the path where create fails.

Verification

Toolchain: GOTOOLCHAIN=go1.26.6 GOWORK=off GOFLAGS='-p=2 -count=1' AWS_EC2_METADATA_DISABLED=true

  • New unit test TestMarketplaceList_DBFailureAndCancelFailureKeepsListing (mock store) covers two cases: the listing gets recorded, and the DB stays down. It asserts 502, no "rolled back", the listing ID and AWS error in the message, and no release write ("", "").
  • New integration test TestMarketplaceListCancelFailureKeepsListingRecorded uses real Postgres via testcontainers. The store wrapper fails only the first listing write, and the EC2 client is a stub. The test asserts the row holds ril-default/active, a second list attempt is refused without calling AWS create again, and a later successful cancel moves the row to cancelled.
  • On the pre-fix handler, both tests fail:
    Messages: listing created but could not be persisted; listing has been rolled back: transient db failure
    --- FAIL: TestMarketplaceListCancelFailureKeepsListingRecorded
    --- FAIL: TestMarketplaceList_DBFailureAndCancelFailureKeepsListing/listing_recorded   mock: Unexpected Method Call
    
  • Mutation check: changing the recording write back to the release ("", "") also fails both. The integration test reports expected: "ril-default" actual: "" and "An error is expected but got nil" (a duplicate listing was created). The mutation was then reverted.
  • With the fix: go build ./..., go vet ./..., go vet -tags integration ./internal/api/ ./internal/config/ all clean. go test ./internal/api/ ./internal/config/ ./internal/server/: 3640 passed. go test -tags integration ./internal/api/ -run Marketplace: 106 passed. golangci-lint run ./internal/api/...: no issues.

All of this evidence uses mocks or fixtures (stub EC2, injected DB failure). No real AWS listing was created.

Deliberately left

Closes #335

When a created listing could not be persisted and the compensating
CancelReservedInstancesListing also failed, reserveAndCreateListing
released the claim (restoring the pre-listing fields) and reported the
listing as rolled back, leaving a live AWS listing the platform no
longer tracked and a row from which a duplicate listing could be made.

On that branch, record the known listing ID and state instead of
releasing the claim, so marketplace-cancel can reach it, and return a
502 naming the listing and the AWS cancel error. If that write also
fails, the row keeps its pending claim, which blocks a second listing,
and the message says an operator must reconcile it.

Closes #335
@cristim cristim added priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/few Limited audience effort/s Hours type/bug Defect triaged Item has been triaged labels Oct 5, 2026
@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 3 billable files and costs up to $0.75.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Or wait 1 minute for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available. Your 50 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-platform/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 385755ac-d7f7-4850-b3bc-3a5b4e64c4ff
📥 Commits

Reviewing files that changed from the base of the PR and between 87b8ce8 and a4557a7.

📒 Files selected for processing (3)
  • internal/api/handler_marketplace.go
  • internal/api/handler_marketplace_compensation_integration_test.go
  • internal/api/handler_marketplace_test.go
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

The compensating cancel and the second DB write reused the request
context. A request deadline or client disconnect is the likeliest cause
of the first write failing, so both would fail too, leaving the row
pending with no listing ID. Run them under a detached 10s context.

When the recorded AWS state is not active, say the listing needs
reconciliation instead of advising another cancel (a pending listing
cannot be canceled through the API). An empty AWS state is recorded as
pending, since the claim SQL treats an empty state as free.
@cristim
cristim merged commit e27e857 into main Oct 5, 2026
25 checks passed
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/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(api): preserve marketplace listing state when compensating cancellation fails

1 participant