@@ -18,8 +18,11 @@ package async
1818
1919import (
2020 "context"
21+ "net/http"
22+ "strconv"
2123 "time"
2224
25+ "github.com/Azure/go-autorest/autorest"
2326 azureautorest "github.com/Azure/go-autorest/autorest/azure"
2427 "github.com/pkg/errors"
2528 infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
@@ -76,7 +79,7 @@ func processOngoingOperation(ctx context.Context, scope FutureScope, client Futu
7679
7780 // Operation is still in progress, update conditions and requeue.
7881 log .V (2 ).Info ("long running operation is still ongoing" , "service" , serviceName , "resource" , resourceName )
79- return nil , azure .WithTransientError (azure .NewOperationNotDoneError (future ), retryAfter (sdkFuture ))
82+ return nil , azure .WithTransientError (azure .NewOperationNotDoneError (future ), getRequeueAfterFromFuture (sdkFuture ))
8083 }
8184 if err != nil {
8285 log .V (2 ).Error (err , "error checking long running operation status after it finished" )
@@ -111,7 +114,8 @@ func (s *Service) CreateResource(ctx context.Context, spec azure.ResourceSpecGet
111114 // Get the resource if it already exists, and use it to construct the desired resource parameters.
112115 var existingResource interface {}
113116 if existing , err := s .Creator .Get (ctx , spec ); err != nil && ! azure .ResourceNotFound (err ) {
114- return nil , errors .Wrapf (err , "failed to get existing resource %s/%s (service: %s)" , rgName , resourceName , serviceName )
117+ errWrapped := errors .Wrapf (err , "failed to get existing resource %s/%s (service: %s)" , rgName , resourceName , serviceName )
118+ return nil , azure .WithTransientError (errWrapped , getRetryAfterFromError (err ))
115119 } else if err == nil {
116120 existingResource = existing
117121 log .V (2 ).Info ("successfully got existing resource" , "service" , serviceName , "resource" , resourceName , "resourceGroup" , rgName )
@@ -136,7 +140,7 @@ func (s *Service) CreateResource(ctx context.Context, spec azure.ResourceSpecGet
136140 return nil , errors .Wrapf (err , "failed to create resource %s/%s (service: %s)" , rgName , resourceName , serviceName )
137141 }
138142 s .Scope .SetLongRunningOperationState (future )
139- return nil , azure .WithTransientError (azure .NewOperationNotDoneError (future ), retryAfter (sdkFuture ))
143+ return nil , azure .WithTransientError (azure .NewOperationNotDoneError (future ), getRequeueAfterFromFuture (sdkFuture ))
140144 } else if err != nil {
141145 return nil , errors .Wrapf (err , "failed to create resource %s/%s (service: %s)" , rgName , resourceName , serviceName )
142146 }
@@ -170,7 +174,7 @@ func (s *Service) DeleteResource(ctx context.Context, spec azure.ResourceSpecGet
170174 return errors .Wrapf (err , "failed to delete resource %s/%s (service: %s)" , rgName , resourceName , serviceName )
171175 }
172176 s .Scope .SetLongRunningOperationState (future )
173- return azure .WithTransientError (azure .NewOperationNotDoneError (future ), retryAfter (sdkFuture ))
177+ return azure .WithTransientError (azure .NewOperationNotDoneError (future ), getRequeueAfterFromFuture (sdkFuture ))
174178 } else if err != nil {
175179 if azure .ResourceNotFound (err ) {
176180 // already deleted
@@ -183,12 +187,40 @@ func (s *Service) DeleteResource(ctx context.Context, spec azure.ResourceSpecGet
183187 return nil
184188}
185189
186- // retryAfter returns the max between the `RETRY-AFTER` header and the default requeue time.
190+ // getRequeueAfterFromFuture returns the max between the `RETRY-AFTER` header and the default requeue time.
187191// This ensures we respect the retry-after header if it is set and avoid retrying too often during an API throttling event.
188- func retryAfter (sdkFuture azureautorest.FutureAPI ) time.Duration {
192+ func getRequeueAfterFromFuture (sdkFuture azureautorest.FutureAPI ) time.Duration {
189193 retryAfter , _ := sdkFuture .GetPollingDelay ()
190194 if retryAfter < reconciler .DefaultReconcilerRequeue {
191195 retryAfter = reconciler .DefaultReconcilerRequeue
192196 }
193197 return retryAfter
194198}
199+
200+ // getRetryAfterFromError returns the time.Duration from the http.Response in the autorest.DetailedError.
201+ // If there is no Response object, or if there is no meaningful Retry-After header data, we return a default.
202+ func getRetryAfterFromError (err error ) time.Duration {
203+ // In case we aren't able to introspect Retry-After from the error type, we'll return this default
204+ ret := reconciler .DefaultReconcilerRequeue
205+ var detailedError autorest.DetailedError
206+ // if we have a strongly typed autorest.DetailedError then we can introspect the HTTP response data
207+ if errors .As (err , & detailedError ) {
208+ if detailedError .Response != nil {
209+ // If we have Retry-After HTTP header data for any reason, prefer it
210+ if retryAfter := detailedError .Response .Header .Get ("Retry-After" ); retryAfter != "" {
211+ // This handles the case where Retry-After data is in the form of units of seconds
212+ if rai , err := strconv .Atoi (retryAfter ); err == nil {
213+ ret = time .Duration (rai ) * time .Second
214+ // This handles the case where Retry-After data is in the form of absolute time
215+ } else if t , err := time .Parse (time .RFC1123 , retryAfter ); err == nil {
216+ ret = time .Until (t )
217+ }
218+ // If we didn't find Retry-After HTTP header data but the response type is 429,
219+ // we'll have to come up with our sane default.
220+ } else if detailedError .Response .StatusCode == http .StatusTooManyRequests {
221+ ret = reconciler .DefaultHTTP429RetryAfter
222+ }
223+ }
224+ }
225+ return ret
226+ }
0 commit comments