Skip to content

Commit 4741932

Browse files
authored
Fix Retry middleware not releasing retry token (#1560)
Updates the Retry middleware to release the retry token, on subsequent attempts. This fixes #1413, and is based on PR #1424.
1 parent e4d9a88 commit 4741932

File tree

5 files changed

+245
-125
lines changed

5 files changed

+245
-125
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "781710a7-ecb2-4b92-90b2-642bd90b42c9",
3+
"type": "bugfix",
4+
"description": "Updates the Retry middleware to release the retry token, on subsequent attempts. This fixes #1413, and is based on PR #1424",
5+
"modules": [
6+
"."
7+
]
8+
}

aws/retry/middleware.go

Lines changed: 81 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import (
1616
"github.com/aws/smithy-go/transport/http"
1717
)
1818

19-
// RequestCloner is a function that can take an input request type and clone the request
20-
// for use in a subsequent retry attempt
19+
// RequestCloner is a function that can take an input request type and clone
20+
// the request for use in a subsequent retry attempt.
2121
type RequestCloner func(interface{}) interface{}
2222

2323
type retryMetadata struct {
@@ -27,11 +27,12 @@ type retryMetadata struct {
2727
AttemptClockSkew time.Duration
2828
}
2929

30-
// Attempt is a Smithy FinalizeMiddleware that handles retry attempts using the provided
31-
// Retryer implementation
30+
// Attempt is a Smithy Finalize middleware that handles retry attempts using
31+
// the provided Retryer implementation.
3232
type Attempt struct {
33-
// Enable the logging of retry attempts performed by the SDK.
34-
// This will include logging retry attempts, unretryable errors, and when max attempts are reached.
33+
// Enable the logging of retry attempts performed by the SDK. This will
34+
// include logging retry attempts, unretryable errors, and when max
35+
// attempts are reached.
3536
LogAttempts bool
3637

3738
retryer aws.Retryer
@@ -59,21 +60,24 @@ func (r Attempt) logf(logger logging.Logger, classification logging.Classificati
5960
logger.Logf(classification, format, v...)
6061
}
6162

62-
// HandleFinalize utilizes the provider Retryer implementation to attempt retries over the next handler
63-
func (r Attempt) HandleFinalize(ctx context.Context, in smithymiddle.FinalizeInput, next smithymiddle.FinalizeHandler) (
63+
// HandleFinalize utilizes the provider Retryer implementation to attempt
64+
// retries over the next handler
65+
func (r *Attempt) HandleFinalize(ctx context.Context, in smithymiddle.FinalizeInput, next smithymiddle.FinalizeHandler) (
6466
out smithymiddle.FinalizeOutput, metadata smithymiddle.Metadata, err error,
6567
) {
6668
var attemptNum int
6769
var attemptClockSkew time.Duration
6870
var attemptResults AttemptResults
6971

7072
maxAttempts := r.retryer.MaxAttempts()
73+
releaseRetryToken := nopRelease
7174

7275
for {
7376
attemptNum++
7477
attemptInput := in
7578
attemptInput.Request = r.requestCloner(attemptInput.Request)
7679

80+
// Record the metadata for the for attempt being started.
7781
attemptCtx := setRetryMetadata(ctx, retryMetadata{
7882
AttemptNum: attemptNum,
7983
AttemptTime: sdk.NowTime().UTC(),
@@ -82,23 +86,20 @@ func (r Attempt) HandleFinalize(ctx context.Context, in smithymiddle.FinalizeInp
8286
})
8387

8488
var attemptResult AttemptResult
89+
out, attemptResult, releaseRetryToken, err = r.handleAttempt(attemptCtx, attemptInput, releaseRetryToken, next)
90+
attemptClockSkew, _ = awsmiddle.GetAttemptSkew(attemptResult.ResponseMetadata)
8591

86-
out, attemptResult, err = r.handleAttempt(attemptCtx, attemptInput, next)
87-
88-
var ok bool
89-
attemptClockSkew, ok = awsmiddle.GetAttemptSkew(attemptResult.ResponseMetadata)
90-
if !ok {
91-
attemptClockSkew = 0
92-
}
93-
92+
// AttempResult Retried states that the attempt was not successful, and
93+
// should be retried.
9494
shouldRetry := attemptResult.Retried
9595

96-
// add attempt metadata to list of all attempt metadata
96+
// Add attempt metadata to list of all attempt metadata
9797
attemptResults.Results = append(attemptResults.Results, attemptResult)
9898

9999
if !shouldRetry {
100100
// Ensure the last response's metadata is used as the bases for result
101-
// metadata returned by the stack.
101+
// metadata returned by the stack. The Slice of attempt results
102+
// will be added to this cloned metadata.
102103
metadata = attemptResult.ResponseMetadata.Clone()
103104

104105
break
@@ -110,26 +111,36 @@ func (r Attempt) HandleFinalize(ctx context.Context, in smithymiddle.FinalizeInp
110111
}
111112

112113
// handleAttempt handles an individual request attempt.
113-
func (r Attempt) handleAttempt(ctx context.Context, in smithymiddle.FinalizeInput, next smithymiddle.FinalizeHandler) (
114-
out smithymiddle.FinalizeOutput, attemptResult AttemptResult, err error,
114+
func (r *Attempt) handleAttempt(
115+
ctx context.Context, in smithymiddle.FinalizeInput, releaseRetryToken func(error) error, next smithymiddle.FinalizeHandler,
116+
) (
117+
out smithymiddle.FinalizeOutput, attemptResult AttemptResult, _ func(error) error, err error,
115118
) {
116119
defer func() {
117120
attemptResult.Err = err
118121
}()
119122

120-
relRetryToken := r.retryer.GetInitialToken()
123+
//------------------------------
124+
// Get Initial (aka Send) Token
125+
//------------------------------
126+
releaseInitialToken := r.retryer.GetInitialToken()
127+
128+
//------------------------------
129+
// Send Attempt
130+
//------------------------------
121131
logger := smithymiddle.GetLogger(ctx)
122132
service, operation := awsmiddle.GetServiceID(ctx), awsmiddle.GetOperationName(ctx)
123-
124133
retryMetadata, _ := getRetryMetadata(ctx)
125134
attemptNum := retryMetadata.AttemptNum
126135
maxAttempts := retryMetadata.MaxAttempts
127136

137+
// Following attempts must ensure the request payload stream starts in a
138+
// rewound state.
128139
if attemptNum > 1 {
129140
if rewindable, ok := in.Request.(interface{ RewindStream() error }); ok {
130141
if rewindErr := rewindable.RewindStream(); rewindErr != nil {
131142
err = fmt.Errorf("failed to rewind transport stream for retry, %w", rewindErr)
132-
return out, attemptResult, err
143+
return out, attemptResult, nopRelease, err
133144
}
134145
}
135146

@@ -140,51 +151,78 @@ func (r Attempt) handleAttempt(ctx context.Context, in smithymiddle.FinalizeInpu
140151
out, metadata, err = next.HandleFinalize(ctx, in)
141152
attemptResult.ResponseMetadata = metadata
142153

143-
if releaseError := relRetryToken(err); releaseError != nil && err != nil {
144-
err = fmt.Errorf("failed to release token after request error, %w", err)
145-
return out, attemptResult, err
154+
//------------------------------
155+
// Bookkeeping
156+
//------------------------------
157+
// Release the initial send token based on the state of the attempt's error (if any).
158+
if releaseError := releaseInitialToken(err); releaseError != nil && err != nil {
159+
err = fmt.Errorf("failed to release initial token after request error, %w", err)
160+
return out, attemptResult, nopRelease, err
146161
}
147-
162+
// Release the retry token based on the state of the attempt's error (if any).
163+
if releaseError := releaseRetryToken(err); releaseError != nil && err != nil {
164+
err = fmt.Errorf("failed to release retry token after request error, %w", err)
165+
return out, attemptResult, nopRelease, err
166+
}
167+
// If there was no error making the attempt, nothing further to do. There
168+
// will be nothing to retry.
148169
if err == nil {
149-
return out, attemptResult, err
170+
return out, attemptResult, nopRelease, err
150171
}
151172

173+
//------------------------------
174+
// Is Retryable and Should Retry
175+
//------------------------------
176+
// If the attempt failed with an unretryable error, nothing further to do
177+
// but return, and inform the caller about the terminal failure.
152178
retryable := r.retryer.IsErrorRetryable(err)
153179
if !retryable {
154180
r.logf(logger, logging.Debug, "request failed with unretryable error %v", err)
155-
return out, attemptResult, err
181+
return out, attemptResult, nopRelease, err
156182
}
157183

158184
// set retryable to true
159185
attemptResult.Retryable = true
160186

187+
// Once the maximum number of attempts have been exhausted there is nothing
188+
// further to do other than inform the caller about the terminal failure.
161189
if maxAttempts > 0 && attemptNum >= maxAttempts {
162190
r.logf(logger, logging.Debug, "max retry attempts exhausted, max %d", maxAttempts)
163191
err = &MaxAttemptsError{
164192
Attempt: attemptNum,
165193
Err: err,
166194
}
167-
return out, attemptResult, err
195+
return out, attemptResult, nopRelease, err
168196
}
169197

170-
relRetryToken, reqErr := r.retryer.GetRetryToken(ctx, err)
171-
if reqErr != nil {
172-
return out, attemptResult, reqErr
198+
//------------------------------
199+
// Get Retry (aka Retry Quota) Token
200+
//------------------------------
201+
// Get a retry token that will be released after the
202+
releaseRetryToken, retryTokenErr := r.retryer.GetRetryToken(ctx, err)
203+
if retryTokenErr != nil {
204+
return out, attemptResult, nopRelease, retryTokenErr
173205
}
174206

207+
//------------------------------
208+
// Retry Delay and Sleep
209+
//------------------------------
210+
// Get the retry delay before another attempt can be made, and sleep for
211+
// that time. Potentially early exist if the sleep is canceled via the
212+
// context.
175213
retryDelay, reqErr := r.retryer.RetryDelay(attemptNum, err)
176214
if reqErr != nil {
177-
return out, attemptResult, reqErr
215+
return out, attemptResult, releaseRetryToken, reqErr
178216
}
179-
180217
if reqErr = sdk.SleepWithContext(ctx, retryDelay); reqErr != nil {
181218
err = &aws.RequestCanceledError{Err: reqErr}
182-
return out, attemptResult, err
219+
return out, attemptResult, releaseRetryToken, err
183220
}
184221

222+
// The request should be re-attempted.
185223
attemptResult.Retried = true
186224

187-
return out, attemptResult, err
225+
return out, attemptResult, releaseRetryToken, err
188226
}
189227

190228
// MetricsHeader attaches SDK request metric header for retries to the transport
@@ -195,7 +233,7 @@ func (r *MetricsHeader) ID() string {
195233
return "RetryMetricsHeader"
196234
}
197235

198-
// HandleFinalize attaches the sdk request metric header to the transport layer
236+
// HandleFinalize attaches the SDK request metric header to the transport layer
199237
func (r MetricsHeader) HandleFinalize(ctx context.Context, in smithymiddle.FinalizeInput, next smithymiddle.FinalizeHandler) (
200238
out smithymiddle.FinalizeOutput, metadata smithymiddle.Metadata, err error,
201239
) {
@@ -251,13 +289,14 @@ func setRetryMetadata(ctx context.Context, metadata retryMetadata) context.Conte
251289
return middleware.WithStackValue(ctx, retryMetadataKey{}, metadata)
252290
}
253291

254-
// AddRetryMiddlewaresOptions is the set of options that can be passed to AddRetryMiddlewares for configuring retry
255-
// associated middleware.
292+
// AddRetryMiddlewaresOptions is the set of options that can be passed to
293+
// AddRetryMiddlewares for configuring retry associated middleware.
256294
type AddRetryMiddlewaresOptions struct {
257295
Retryer aws.Retryer
258296

259-
// Enable the logging of retry attempts performed by the SDK.
260-
// This will include logging retry attempts, unretryable errors, and when max attempts are reached.
297+
// Enable the logging of retry attempts performed by the SDK. This will
298+
// include logging retry attempts, unretryable errors, and when max
299+
// attempts are reached.
261300
LogRetryAttempts bool
262301
}
263302

0 commit comments

Comments
 (0)