Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion providers/aws/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/LeanerCloud/cloud-commitments-go/providers/aws
go 1.26.9

require (
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261016194019-89772cf80f67
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261017020613-36d8b1100c05
github.com/aws/aws-sdk-go-v2 v1.41.5
github.com/aws/aws-sdk-go-v2/config v1.29.12
github.com/aws/aws-sdk-go-v2/credentials v1.17.65
Expand Down
4 changes: 2 additions & 2 deletions providers/aws/go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261016194019-89772cf80f67 h1:zpr+lPSdlXeA9ySU7FqHtgn4ADinmRhfJ5Y6jm9ankw=
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261016194019-89772cf80f67/go.mod h1:SCDEgHG6evjPh0RsoXof+J3aMmGXNFMrwbJIle7rMpQ=
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261017020613-36d8b1100c05 h1:YtSRyJHVUTDnxykCGYpGX7Pt/z+mq4/sCphPmO6Pj5M=
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261017020613-36d8b1100c05/go.mod h1:SCDEgHG6evjPh0RsoXof+J3aMmGXNFMrwbJIle7rMpQ=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/config v1.29.12 h1:Y/2a+jLPrPbHpFkpAAYkVEtJmxORlXoo5k2g1fa2sUo=
Expand Down
28 changes: 23 additions & 5 deletions providers/aws/ladder/reshape.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"math"

"github.com/LeanerCloud/cloud-commitments-go/pkg/common"
"github.com/LeanerCloud/cloud-commitments-go/pkg/exchange"
"github.com/LeanerCloud/cloud-commitments-go/pkg/ladder"
)
Expand Down Expand Up @@ -165,6 +166,8 @@ func summarizeExchangeResult(result *exchange.AutoExchangeResult) (ladder.Reshap
len(result.Completed)+len(result.Pending)+len(result.Failed)+len(result.Skipped)),
}

unknown := 0
firstUnknown := -1
for i := range result.Completed {
o := &result.Completed[i]
summary.Details = append(summary.Details, fmt.Sprintf(
Expand All @@ -179,9 +182,17 @@ func summarizeExchangeResult(result *exchange.AutoExchangeResult) (ladder.Reshap
}
for i := range result.Failed {
o := &result.Failed[i]
label := "failed"
if o.OutcomeUnknown {
unknown++
if firstUnknown < 0 {
firstUnknown = i
}
label = "outcome unknown, reconcile before retrying"
}
summary.Details = append(summary.Details, fmt.Sprintf(
"failed: %s (%s) -> %s: %s",
o.SourceRIID, o.SourceInstanceType, o.TargetInstanceType, o.Error))
"%s: %s (%s) -> %s: %s",
label, o.SourceRIID, o.SourceInstanceType, o.TargetInstanceType, o.Error))
}
for i := range result.Skipped {
s := &result.Skipped[i]
Expand All @@ -194,9 +205,16 @@ func summarizeExchangeResult(result *exchange.AutoExchangeResult) (ladder.Reshap
// summary.Analyzed: pending and skipped items were never attempted,
// and an inflated denominator would misread during an incident.
attempts := len(result.Completed) + len(result.Failed)
return summary, fmt.Errorf(
"ReshapeBuffer: %d of %d exchange attempt(s) failed (first: %s: %s); see summary details for the full list",
len(result.Failed), attempts, result.Failed[0].SourceRIID, result.Failed[0].Error)
const format = "ReshapeBuffer: %d of %d exchange attempt(s) failed (first: %s: %s)%s; see summary details for the full list"
if unknown == 0 {
first := &result.Failed[0]
return summary, fmt.Errorf(format, len(result.Failed), attempts, first.SourceRIID, first.Error, "")
}
// The unknown entry is the one needing reconciliation, so name it
// and wrap the sentinel for errors.Is.
first := &result.Failed[firstUnknown]
return summary, fmt.Errorf(format+": %w", len(result.Failed), attempts, first.SourceRIID, first.Error,
fmt.Sprintf("; %d with unknown outcome", unknown), common.ErrOutcomeUnknown)
}
return summary, nil
}
40 changes: 40 additions & 0 deletions providers/aws/ladder/reshape_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ func TestReshapeBuffer_PartialFailure_SummaryPlusError(t *testing.T) {
assert.Contains(t, err.Error(), "1 of 2 exchange attempt(s) failed",
"denominator must be completed+failed attempts, not Analyzed")
assert.Contains(t, err.Error(), "ri-bad")
assert.NotContains(t, err.Error(), "unknown outcome", "suffix only when K > 0")
require.NotErrorIs(t, err, common.ErrOutcomeUnknown, "definite failures must not claim an unknown outcome")

// The summary is still populated for audit alongside the error.
assert.Equal(t, 3, summary.Analyzed)
Expand Down Expand Up @@ -310,3 +312,41 @@ func TestReshapeBuffer_WrongScope_ReturnsError(t *testing.T) {
assert.Contains(t, err.Error(), "does not match configured account")
assert.Equal(t, 0, ex.calls)
}

func TestReshapeBuffer_UnknownOutcome_RenderedAsUnknownNotFailed(t *testing.T) {
ex := &fakeExchangeRunner{result: &exchange.AutoExchangeResult{
Mode: string(exchange.ExchangeModeAuto),
Failed: []exchange.ExchangeOutcome{
{SourceRIID: "ri-bad", SourceInstanceType: "m5.large", TargetInstanceType: "m5.xlarge", Error: "rejected"},
{SourceRIID: "ri-unk", SourceInstanceType: "c5.large", TargetInstanceType: "c5.xlarge", Error: "accept timed out", OutcomeUnknown: true},
},
}}
a := newWiredLadder(t, &fakePurchaser{}, &fakePurchaser{}, ex)

summary, err := a.ReshapeBuffer(context.Background(), testScope(), validReshapeCfg())
require.Error(t, err)
require.ErrorIs(t, err, common.ErrOutcomeUnknown)
assert.Contains(t, err.Error(), "2 of 2 exchange attempt(s) failed (first: ri-unk: accept timed out)",
"first: names the unknown entry even when a definite failure precedes it")
assert.Contains(t, err.Error(), "; 1 with unknown outcome")

require.Len(t, summary.Details, 2)
assert.Contains(t, summary.Details[0], "failed: ri-bad")
assert.NotContains(t, summary.Details[0], "outcome unknown")
assert.Contains(t, summary.Details[1], "outcome unknown, reconcile before retrying: ri-unk")
assert.NotContains(t, summary.Details[1], "failed:")
}

func TestReshapeBuffer_UnknownOnly_WrapsErrOutcomeUnknown(t *testing.T) {
ex := &fakeExchangeRunner{result: &exchange.AutoExchangeResult{
Mode: string(exchange.ExchangeModeAuto),
Failed: []exchange.ExchangeOutcome{
{SourceRIID: "ri-unk", SourceInstanceType: "c5.large", TargetInstanceType: "c5.xlarge", Error: "accept timed out", OutcomeUnknown: true},
},
}}
a := newWiredLadder(t, &fakePurchaser{}, &fakePurchaser{}, ex)

_, err := a.ReshapeBuffer(context.Background(), testScope(), validReshapeCfg())
require.ErrorIs(t, err, common.ErrOutcomeUnknown)
assert.Contains(t, err.Error(), "first: ri-unk: accept timed out")
}
Loading