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
54 changes: 51 additions & 3 deletions s3/s3_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -40,6 +41,13 @@ const (
// AWSRetryMaximumBackoff specifies the maximum duration between retried attempts.
AWSRetryMaximumBackoff = 300 * time.Second

// EnvAWSRetryMaxAttempts overrides AWSRetryMaxAttempts when set to a positive integer.
EnvAWSRetryMaxAttempts = "AWS_RETRY_MAX_ATTEMPTS"
// EnvAWSRetryMaximumAttempts overrides AWSRetryMaximumAttempts when set to a positive integer.
EnvAWSRetryMaximumAttempts = "AWS_RETRY_MAXIMUM_ATTEMPTS"
// EnvAWSRetryMaximumBackoff overrides AWSRetryMaximumBackoff when set to a Go duration string (e.g. "60s", "5m").
EnvAWSRetryMaximumBackoff = "AWS_RETRY_MAXIMUM_BACKOFF"
Comment on lines +45 to +49

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @alliasgher,
Could you try to add the new environment variables into this function setupS3Credential in the util/credential.go as AWSSecretKey?
Then we can allow users to set up the parameters in the Secret and pass them here.


// InvalidRequestErrorMsg is the error message returned by S3 Compatible services when the authorization mechanism is not supported,
// which can be caused by using AWS Signature Version 2 for signing requests to AWS S3 regions that require AWS Signature Version 4.
InvalidRequestErrorMsg = "The authorization mechanism you have provided is not supported. Please use AWS4-HMAC-SHA256."
Expand All @@ -52,6 +60,41 @@ const (
maxSinglePutObjectSize int64 = 5 * 1024 * 1024 * 1024
)

// retryMaxAttempts returns the configured retry max attempts, falling back to
// AWSRetryMaxAttempts when the env var is unset, empty, or malformed.
func retryMaxAttempts() int {
if v := os.Getenv(EnvAWSRetryMaxAttempts); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
return n
}
}
return AWSRetryMaxAttempts
}

// retryMaximumAttempts returns the configured retry maximum attempts, falling
// back to AWSRetryMaximumAttempts when the env var is unset, empty, or
// malformed.
func retryMaximumAttempts() int {
if v := os.Getenv(EnvAWSRetryMaximumAttempts); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
return n
}
}
return AWSRetryMaximumAttempts
}

// retryMaximumBackoff returns the configured retry maximum backoff, falling
// back to AWSRetryMaximumBackoff when the env var is unset, empty, or
// malformed.
func retryMaximumBackoff() time.Duration {
if v := os.Getenv(EnvAWSRetryMaximumBackoff); v != "" {
if d, err := time.ParseDuration(v); err == nil && d > 0 {
return d
}
}
return AWSRetryMaximumBackoff
}

func newService(u *url.URL) (*service, error) {
s := service{}
if u.User != nil {
Expand Down Expand Up @@ -82,7 +125,7 @@ func (s *service) newInstance(ctx context.Context, retryBackoff bool) (*s3.Clien
// Load AWS configuration
cfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion(s.Region),
config.WithRetryMaxAttempts(AWSRetryMaxAttempts),
config.WithRetryMaxAttempts(retryMaxAttempts()),
config.WithRequestChecksumCalculation(aws.RequestChecksumCalculationWhenRequired),
config.WithResponseChecksumValidation(aws.ResponseChecksumValidationWhenRequired),
)
Expand Down Expand Up @@ -114,9 +157,14 @@ func (s *service) newInstance(ctx context.Context, retryBackoff bool) (*s3.Clien
o.UsePathStyle = usePathStyle
if retryBackoff {
o.Retryer = retry.NewStandard(func(so *retry.StandardOptions) {
so.MaxAttempts = AWSRetryMaximumAttempts
so.MaxBackoff = AWSRetryMaximumBackoff
so.MaxAttempts = retryMaximumAttempts()
so.MaxBackoff = retryMaximumBackoff()
Comment on lines 158 to +161

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 21c2977. Reproduced with the vendored SDK: effective MaxAttempts was 5 with AWS_RETRY_MAXIMUM_ATTEMPTS=20, and 20 once o.RetryMaxAttempts is cleared.

})
// NewFromConfig runs finalizeRetryMaxAttempts after this callback, which
// wraps the retryer above in retry.AddWithMaxAttempts(o.RetryMaxAttempts)
// and would cap it at AWS_RETRY_MAX_ATTEMPTS. Clear it so the retryer's
// own AWS_RETRY_MAXIMUM_ATTEMPTS stays effective.
o.RetryMaxAttempts = 0
}
// Google Cloud Storage alters the `Accept-Encoding` header (GCS might changes the header on its way to GCS by appending gzip(gfe) as accepted encoding),
// which causing signature mismatches and breaks the v2 request signature verification.
Expand Down
104 changes: 104 additions & 0 deletions s3/s3_service_retry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package s3

import (
"context"
"testing"
"time"
)

func TestRetryMaxAttempts_EnvOverride(t *testing.T) {
cases := []struct {
name string
env string
want int
}{
{"unset", "", AWSRetryMaxAttempts},
{"valid", "7", 7},
{"zero falls back", "0", AWSRetryMaxAttempts},
{"negative falls back", "-1", AWSRetryMaxAttempts},
{"garbage falls back", "abc", AWSRetryMaxAttempts},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Setenv(EnvAWSRetryMaxAttempts, tc.env)
if got := retryMaxAttempts(); got != tc.want {
t.Fatalf("retryMaxAttempts() = %d, want %d", got, tc.want)
}
})
}
}

func TestRetryMaximumAttempts_EnvOverride(t *testing.T) {
cases := []struct {
name string
env string
want int
}{
{"unset", "", AWSRetryMaximumAttempts},
{"valid", "20", 20},
{"zero falls back", "0", AWSRetryMaximumAttempts},
{"garbage falls back", "x", AWSRetryMaximumAttempts},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Setenv(EnvAWSRetryMaximumAttempts, tc.env)
if got := retryMaximumAttempts(); got != tc.want {
t.Fatalf("retryMaximumAttempts() = %d, want %d", got, tc.want)
Comment on lines +44 to +46

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 21c2977 as TestRetryMaximumAttempts_AppliedToClient, covering both the backoff and non-backoff branches.

}
})
}
}

func TestRetryMaximumBackoff_EnvOverride(t *testing.T) {
cases := []struct {
name string
env string
want time.Duration
}{
{"unset", "", AWSRetryMaximumBackoff},
{"valid seconds", "60s", 60 * time.Second},
{"valid minutes", "5m", 5 * time.Minute},
{"zero falls back", "0s", AWSRetryMaximumBackoff},
{"garbage falls back", "nope", AWSRetryMaximumBackoff},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Setenv(EnvAWSRetryMaximumBackoff, tc.env)
if got := retryMaximumBackoff(); got != tc.want {
t.Fatalf("retryMaximumBackoff() = %v, want %v", got, tc.want)
}
})
}
}

// The env-parsing tests above pass whether or not the retryer is actually wired
// into the SDK, so assert the effective value on the constructed client.
// s3.NewFromConfig runs finalizeRetryMaxAttempts after the option callback and
// caps a custom retryer with o.RetryMaxAttempts, which would silently limit
// AWS_RETRY_MAXIMUM_ATTEMPTS to AWS_RETRY_MAX_ATTEMPTS.
func TestRetryMaximumAttempts_AppliedToClient(t *testing.T) {
t.Setenv(EnvAWSRetryMaxAttempts, "5")
t.Setenv(EnvAWSRetryMaximumAttempts, "20")
t.Setenv("AWS_REGION", "us-east-1")
t.Setenv("AWS_ACCESS_KEY_ID", "test")
t.Setenv("AWS_SECRET_ACCESS_KEY", "test")

s := &service{Region: "us-east-1"}

client, err := s.newInstance(context.Background(), true)
if err != nil {
t.Fatalf("newInstance() error = %v", err)
}
if got := client.Options().Retryer.MaxAttempts(); got != 20 {
t.Errorf("with retry backoff, Retryer.MaxAttempts() = %d, want 20", got)
}

// Without the custom retryer, AWS_RETRY_MAX_ATTEMPTS still governs.
client, err = s.newInstance(context.Background(), false)
if err != nil {
t.Fatalf("newInstance() error = %v", err)
}
if got := client.Options().Retryer.MaxAttempts(); got != 5 {
t.Errorf("without retry backoff, Retryer.MaxAttempts() = %d, want 5", got)
}
}