Repository navigation
fix(purchase): show summarized purchase failures and keep provider details server-side #1712
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
1ff5fcb
feat(purchase): summarize provider purchase errors into fixed categories
cristim 12021c9
fix(api): show summarized purchase failures and keep provider details…
cristim 88cd56f
fix(history): show the duplicate-purchase warning only for unknown ou…
cristim 2764831
fix(history): keep the duplicate-purchase warning unless every rec wa…
cristim 37b5e6a
fix(purchase): pass through only known server notes in stored errors
cristim 4d37565
test(purchase): cover codeless Azure, GCP and AWS failure shapes
cristim b851ab5
test(purchase): name the provider-detail test helpers plainly
cristim 1c2df30
fix(purchase): show only the fixed head of server notes
cristim 569f832
fix(purchase): return only a sentinel's own text
cristim 5704b52
fix(history): keep Retry hidden for refused AWS purchases
cristim 9b2a69d
fix(purchase): read every GCP reason and keep post-insert waits uncon…
cristim 5bc1aad
style(purchase): preallocate the matched code groups
cristim File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package api | ||
|
|
||
| import ( | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/LeanerCloud/cloud-commitments-platform/internal/config" | ||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| func TestHistoryRow_DuplicateGuardHint(t *testing.T) { | ||
| ec2 := []config.RecommendationRecord{{Provider: "aws", Service: "ec2"}} | ||
| denied := executionToHistoryRow(config.PurchaseExecution{Status: "failed", Error: fakeAWSDenied, Recommendations: ec2}, "", "") | ||
| assert.False(t, strings.Contains(denied.OpsHint, "duplicate guard"), "definite refusal hides the warning") | ||
| unknown := executionToHistoryRow(config.PurchaseExecution{Status: "failed", Error: "purchase outcome unknown", Recommendations: ec2}, "", "") | ||
| assert.Contains(t, unknown.OpsHint, "duplicate guard") | ||
| assert.True(t, unknown.RedriveUnsafe) | ||
|
|
||
| // One refused rec does not prove that another, whose outcome is unknown, bought nothing. | ||
| mixed := executionToHistoryRow(config.PurchaseExecution{Status: "failed", Error: "t4g.nano: purchase outcome unknown; t4g.micro: " + fakeAWSDenied, Recommendations: ec2}, "", "") | ||
| assert.Contains(t, mixed.OpsHint, "duplicate guard", "a mixed failure keeps the warning") | ||
| allDenied := executionToHistoryRow(config.PurchaseExecution{Status: "failed", Error: "t4g.nano: " + fakeAWSDenied + "; t4g.micro: " + fakeAWSDenied, Recommendations: ec2}, "", "") | ||
| assert.NotContains(t, allDenied.OpsHint, "duplicate guard", "every rec refused hides the warning") | ||
| } | ||
|
|
||
| func TestHistoryRow_DuplicateHintKeptOutsideDefiniteAWSRefusals(t *testing.T) { | ||
| const gcpRate = "commitment creation failed: googleapi: Error 403: Rate Limit Exceeded, rateLimitExceeded" | ||
| const gcpNotFound = "commitment creation failed: googleapi: Error 404: operations/op-1 was not found, notFound" | ||
| azure500 := azureHTTPError(t, 500, "/providers/Microsoft.Capacity/reservationOrders/x").Error() | ||
| azure429 := "PUT https://management.azure.com/x\nRESPONSE 429: 429 Too Many\nERROR CODE: TooManyRequests" | ||
| aws503 := strings.Replace(fakeAWSDenied, "StatusCode: 403", "StatusCode: 503", 1) | ||
| ec2 := config.RecommendationRecord{Provider: "aws", Service: "ec2"} | ||
| azSP := config.RecommendationRecord{Provider: "azure", Service: "savingsplans"} | ||
| gcp := config.RecommendationRecord{Provider: "gcp", Service: "compute"} | ||
| cases := []struct { | ||
| name string | ||
| recs []config.RecommendationRecord | ||
| err string | ||
| }{ | ||
| {"gcp rate limit", []config.RecommendationRecord{gcp, ec2}, gcpRate}, | ||
| {"gcp not found", []config.RecommendationRecord{gcp, ec2}, gcpNotFound}, | ||
| {"azure 500 on a capacity url", []config.RecommendationRecord{azSP}, azure500}, | ||
| {"azure 429", []config.RecommendationRecord{azSP}, azure429}, | ||
| {"aws 5xx", []config.RecommendationRecord{ec2}, aws503}, | ||
| {"aws without a code", []config.RecommendationRecord{ec2}, "operation error EC2: X, https response error StatusCode: 403"}, | ||
| {"non-aws rec beside a definite aws refusal", []config.RecommendationRecord{ec2, gcp}, fakeAWSDenied}, | ||
| } | ||
| for _, c := range cases { | ||
| row := executionToHistoryRow(config.PurchaseExecution{Status: "failed", Error: c.err, Recommendations: c.recs}, "", "") | ||
| assert.True(t, row.RedriveUnsafe, c.name) | ||
| assert.Contains(t, row.OpsHint, "duplicate guard", c.name) | ||
| } | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| package api | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/LeanerCloud/cloud-commitments-platform/internal/config" | ||
| "github.com/LeanerCloud/cloud-commitments-platform/internal/purchase" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| const fakeConnectErr = "failed to connect to `host=db.internal.example user=cudly_app database=cudly`: " + | ||
| "dial error (dial tcp 10.0.0.5:5432: connect: connection refused)" | ||
|
|
||
| func assertNoConnectText(t *testing.T, where, s string) { | ||
| t.Helper() | ||
| for _, n := range []string{"db.internal.example", "cudly_app", "10.0.0.5", "dial tcp"} { | ||
| assert.NotContains(t, s, n, where) | ||
| } | ||
| } | ||
|
|
||
| func TestTailedNotesWithStoreErrors_NeverReachUserSurfaces(t *testing.T) { | ||
| notes := []string{ | ||
| "ramp not advanced: " + fakeConnectErr, | ||
| "refusing to execute: ladder tranche lookup failed: " + fakeConnectErr, | ||
| "refusing to execute ladder purchase: global config unreadable: " + fakeConnectErr, | ||
| purchase.ErrStaleAzurePricing.Error() + ": cannot read the latest collection state to confirm Azure prices are current: " + fakeConnectErr, | ||
| } | ||
| for _, note := range notes { | ||
| name := note[:strings.Index(note, ":")] | ||
| t.Run(name, func(t *testing.T) { | ||
| row := executionToHistoryRow(config.PurchaseExecution{ExecutionID: "e-1", Status: "failed", Error: note}, "", "") | ||
| assertNoConnectText(t, "status_description", row.StatusDescription) | ||
| assert.NotEmpty(t, row.StatusDescription) | ||
|
|
||
| exec := &config.PurchaseExecution{ExecutionID: "e-1", Status: "failed", Error: note} | ||
| body, err := json.Marshal(buildPurchaseDetailsResponse(exec, "plan")) | ||
| require.NoError(t, err) | ||
| assertNoConnectText(t, "details", string(body)) | ||
|
|
||
| creator := retryCallerID | ||
| failed := &config.PurchaseExecution{ExecutionID: retryExecID, Status: "failed", CreatedByUserID: &creator, | ||
| Error: "FROM_EMAIL not configured for this deployment; " + note} | ||
| handler, _, _ := buildSessionRetryHandler(t, failed, &Session{UserID: retryCallerID}, false, true) | ||
| _, rErr := handler.retryPurchase(t.Context(), sessionRetryReq(), retryExecID) | ||
| ce, ok := IsClientError(rErr) | ||
| require.True(t, ok) | ||
| assert.Equal(t, 409, ce.code) | ||
| raw, mErr := json.Marshal(ce.Details()) | ||
| require.NoError(t, mErr) | ||
| assertNoConnectText(t, "retry 409", string(raw)+ce.Error()) | ||
| assert.NotEmpty(t, ce.Details()["failure_reason"]) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestExecuteFailure_SentinelWrappingProviderErrorShowsOnlySentinelText(t *testing.T) { | ||
| for _, sentinel := range []error{purchase.ErrStaleAzurePricing, purchase.ErrClaimedRowRefused} { | ||
| err := fmt.Errorf("%w: %w", sentinel, errors.New(fakeConnectErr)) | ||
| _, msg := summarizeExecuteFailure("e-1", err) | ||
| assert.Equal(t, sentinel.Error(), msg) | ||
| out := executeFailureError("e-1", "executed", &config.PurchaseExecution{Status: "failed"}, err) | ||
| require.Error(t, out) | ||
| // Non-client errors render as a generic 500 (handler.go), so only | ||
| // structured client errors can carry text to the caller. | ||
| if ce, ok := IsClientError(out); ok { | ||
| assertNoConnectText(t, "live", ce.Error()) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestTokenActionError_StalePricingShowsOnlySentinelText(t *testing.T) { | ||
| err := tokenActionError(fmt.Errorf("%w: %w", purchase.ErrStaleAzurePricing, errors.New(fakeConnectErr))) | ||
| ce, ok := IsClientError(err) | ||
| require.True(t, ok) | ||
| assert.Equal(t, purchase.ErrStaleAzurePricing.Error(), ce.Error()) | ||
| } | ||
|
|
||
| // A refused AWS purchase keeps ops_hint (the History UI hides Retry on it) but | ||
| // carries no duplicate-purchase warning. | ||
| func TestHistoryRow_DefiniteAWSRefusalKeepsOpsHintWithoutDuplicateWarning(t *testing.T) { | ||
| denied := "operation error EC2: X, https response error StatusCode: 403, RequestID: r, api error UnauthorizedOperation: msg" | ||
| row := executionToHistoryRow(config.PurchaseExecution{ | ||
| ExecutionID: "e-1", Status: "failed", Error: denied, | ||
| Recommendations: []config.RecommendationRecord{{Provider: "aws", Service: "ec2"}}, | ||
| }, "", "") | ||
| assert.True(t, row.RedriveUnsafe) | ||
| assert.NotEmpty(t, row.OpsHint) | ||
| assert.NotContains(t, strings.ToLower(row.OpsHint), "duplicate guard") | ||
| assert.NotEqual(t, purchase.RedriveRefusalReason(&config.PurchaseExecution{ | ||
| Recommendations: []config.RecommendationRecord{{Provider: "aws", Service: "ec2"}}}), row.OpsHint) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.