Skip to content
Open
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
14 changes: 14 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ package provider

import (
"context"
"errors"
"fmt"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/retry"
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
"github.com/aws/aws-sdk-go-v2/service/cloudcontrol"
cctypes_sdk "github.com/aws/aws-sdk-go-v2/service/cloudcontrol/types"
awsbase "github.com/hashicorp/aws-sdk-go-base/v2"
basediag "github.com/hashicorp/aws-sdk-go-base/v2/diag"
baselogging "github.com/hashicorp/aws-sdk-go-base/v2/logging"
Expand Down Expand Up @@ -591,6 +595,16 @@ func newProviderData(ctx context.Context, c *configModel) (*providerData, diag.D
if c.Endpoints != nil {
o.BaseEndpoint = flex.StringFromFramework(ctx, c.Endpoints.CloudControlAPI)
}
// Add custom retry for CloudControl throttling
o.Retryer = retry.NewStandard(func(so *retry.StandardOptions) {
so.Retryables = append(so.Retryables, retry.IsErrorRetryableFunc(func(err error) aws.Ternary {
var throttlingErr *cctypes_sdk.ThrottlingException
if errors.As(err, &throttlingErr) {
return aws.TrueTernary
}
return aws.UnknownTernary
}))
})
})

accountID, partitionID, awsDiags := awsbase.GetAwsAccountIDAndPartition(ctx, cfg, &awsbaseConfig)
Expand Down
50 changes: 50 additions & 0 deletions internal/provider/retry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package provider

import (
"errors"
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
cctypes_sdk "github.com/aws/aws-sdk-go-v2/service/cloudcontrol/types"
)

func TestThrottlingRetry(t *testing.T) {
tests := []struct {
name string
err error
expectRetry aws.Ternary
}{
{
name: "ThrottlingException should retry",
err: &cctypes_sdk.ThrottlingException{Message: aws.String("Rate exceeded")},
expectRetry: aws.TrueTernary,
},
{
name: "Non-throttling error should not retry",
err: &cctypes_sdk.InvalidRequestException{Message: aws.String("Invalid request")},
expectRetry: aws.UnknownTernary,
},
{
name: "Generic error should not retry",
err: errors.New("some other error"),
expectRetry: aws.UnknownTernary,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var throttlingErr *cctypes_sdk.ThrottlingException
result := aws.UnknownTernary
if errors.As(tt.err, &throttlingErr) {
result = aws.TrueTernary
}

if result != tt.expectRetry {
t.Errorf("Expected retry=%v, got %v", tt.expectRetry, result)
}
})
}
}
5 changes: 5 additions & 0 deletions internal/service/cloudcontrol/waiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ func RetryGetResourceRequestStatus(pProgressEvent **types.ProgressEvent) func(co
return false, nil
}

// Retry on throttling errors
if progressEvent.ErrorCode == types.HandlerErrorCodeThrottling {
return true, err
}

// Build enhanced error message with hook information
waiterErr := newWaiterErr(string(value), aws.ToString(progressEvent.StatusMessage))
waiterErr.withErrorCode(string(progressEvent.ErrorCode))
Expand Down
39 changes: 39 additions & 0 deletions internal/service/cloudcontrol/waiter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,42 @@ func TestRetryGetResourceRequestStatus_WithHookCompleteFailed(t *testing.T) {
}
}
}

func TestRetryGetResourceRequestStatus_ThrottlingError(t *testing.T) {
retryFunc := tfcloudcontrol.RetryGetResourceRequestStatus(nil)

output := &cloudcontrol.GetResourceRequestStatusOutput{
ProgressEvent: &types.ProgressEvent{
OperationStatus: types.OperationStatusFailed,
ErrorCode: types.HandlerErrorCodeThrottling,
StatusMessage: aws.String("Request throttled"),
},
}

retry, _ := retryFunc(context.Background(), nil, output, nil)

if !retry {
t.Error("Expected retry=true for throttling error, got false")
}
}

func TestRetryGetResourceRequestStatus_NonThrottlingError(t *testing.T) {
retryFunc := tfcloudcontrol.RetryGetResourceRequestStatus(nil)

output := &cloudcontrol.GetResourceRequestStatusOutput{
ProgressEvent: &types.ProgressEvent{
OperationStatus: types.OperationStatusFailed,
ErrorCode: types.HandlerErrorCodeInvalidRequest,
StatusMessage: aws.String("Invalid request"),
},
}

retry, err := retryFunc(context.Background(), nil, output, nil)

if retry {
t.Error("Expected retry=false for non-throttling error, got true")
}
if err == nil {
t.Error("Expected error to be returned, got nil")
}
}