Skip to content
This repository was archived by the owner on Feb 12, 2025. It is now read-only.

Commit ab56713

Browse files
authored
Add WaitForCompletion to Future type. (#194)
* Add WaitForCompletion to Future type. The WaitForCompletion method provides a default implementation that will wait until a future has completed, is cancelled or times out. Fixed a bug in Future.Done() to not update polling status if the HTTP response isn't one of the expected values. Added support to mock framework for returning errors in responses so we can simulate things like transient network failures. * Remove Go 1.6 from travis as it doesn't support the context package. * Fixes based on feedback. * Delete Retry-After header for HTTP 500 response. * Track mock responses and replace hard-coded counts.
1 parent 8efdaa3 commit ab56713

File tree

7 files changed

+245
-19
lines changed

7 files changed

+245
-19
lines changed

.travis.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ go:
66
- 1.9
77
- 1.8
88
- 1.7
9-
- 1.6
109

1110
install:
1211
- go get -u github.com/golang/lint/golint

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# CHANGELOG
22

3+
## v9.4.0
4+
5+
### New Features
6+
7+
- Added WaitForCompletion() to Future as a default polling implementation.
8+
9+
### Bug Fixes
10+
11+
- Method Future.Done() shouldn't update polling status for unexpected HTTP status codes.
12+
313
## v9.3.1
414

515
### Bug Fixes

autorest/autorest.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ const (
8787
// ResponseHasStatusCode returns true if the status code in the HTTP Response is in the passed set
8888
// and false otherwise.
8989
func ResponseHasStatusCode(resp *http.Response, codes ...int) bool {
90+
if resp == nil {
91+
return false
92+
}
9093
return containsInt(codes, resp.StatusCode)
9194
}
9295

autorest/azure/async.go

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package azure
1616

1717
import (
1818
"bytes"
19+
"context"
1920
"encoding/json"
2021
"fmt"
2122
"io/ioutil"
@@ -38,6 +39,8 @@ const (
3839
operationSucceeded string = "Succeeded"
3940
)
4041

42+
var pollingCodes = [...]int{http.StatusAccepted, http.StatusCreated, http.StatusOK}
43+
4144
// Future provides a mechanism to access the status and results of an asynchronous request.
4245
// Since futures are stateful they should be passed by value to avoid race conditions.
4346
type Future struct {
@@ -78,7 +81,7 @@ func (f *Future) Done(sender autorest.Sender) (bool, error) {
7881

7982
resp, err := sender.Do(f.req)
8083
f.resp = resp
81-
if err != nil {
84+
if err != nil || !autorest.ResponseHasStatusCode(resp, pollingCodes[:]...) {
8285
return false, err
8386
}
8487

@@ -117,6 +120,47 @@ func (f Future) GetPollingDelay() (time.Duration, bool) {
117120
return d, true
118121
}
119122

123+
// WaitForCompletion will return when one of the following conditions is met: the long
124+
// running operation has completed, the provided context is cancelled, or the client's
125+
// polling duration has been exceeded. It will retry failed polling attempts based on
126+
// the retry value defined in the client up to the maximum retry attempts.
127+
func (f Future) WaitForCompletion(ctx context.Context, client autorest.Client) error {
128+
ctx, cancel := context.WithTimeout(ctx, client.PollingDuration)
129+
defer cancel()
130+
131+
done, err := f.Done(client)
132+
for attempts := 0; !done; done, err = f.Done(client) {
133+
if attempts >= client.RetryAttempts {
134+
return autorest.NewErrorWithError(err, "azure", "WaitForCompletion", f.resp, "the number of retries has been exceeded")
135+
}
136+
// we want delayAttempt to be zero in the non-error case so
137+
// that DelayForBackoff doesn't perform exponential back-off
138+
var delayAttempt int
139+
var delay time.Duration
140+
if err == nil {
141+
// check for Retry-After delay, if not present use the client's polling delay
142+
var ok bool
143+
delay, ok = f.GetPollingDelay()
144+
if !ok {
145+
delay = client.PollingDelay
146+
}
147+
} else {
148+
// there was an error polling for status so perform exponential
149+
// back-off based on the number of attempts using the client's retry
150+
// duration. update attempts after delayAttempt to avoid off-by-one.
151+
delayAttempt = attempts
152+
delay = client.RetryDuration
153+
attempts++
154+
}
155+
// wait until the delay elapses or the context is cancelled
156+
delayElapsed := autorest.DelayForBackoff(delay, delayAttempt, ctx.Done())
157+
if !delayElapsed {
158+
return autorest.NewErrorWithError(ctx.Err(), "azure", "WaitForCompletion", f.resp, "context has been cancelled")
159+
}
160+
}
161+
return err
162+
}
163+
120164
// if the operation failed the polling state will contain
121165
// error information and implements the error interface
122166
func (f *Future) errorInfo() error {
@@ -152,8 +196,7 @@ func DoPollForAsynchronous(delay time.Duration) autorest.SendDecorator {
152196
if err != nil {
153197
return resp, err
154198
}
155-
pollingCodes := []int{http.StatusAccepted, http.StatusCreated, http.StatusOK}
156-
if !autorest.ResponseHasStatusCode(resp, pollingCodes...) {
199+
if !autorest.ResponseHasStatusCode(resp, pollingCodes[:]...) {
157200
return resp, nil
158201
}
159202

autorest/azure/async_test.go

Lines changed: 136 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ package azure
1515
// limitations under the License.
1616

1717
import (
18+
"context"
1819
"encoding/json"
20+
"errors"
1921
"fmt"
2022
"io/ioutil"
2123
"net/http"
@@ -634,7 +636,7 @@ func TestDoPollForAsynchronous_PollsForStatusAccepted(t *testing.T) {
634636
r, _ := autorest.SendWithSender(client, mocks.NewRequest(),
635637
DoPollForAsynchronous(time.Millisecond))
636638

637-
if client.Attempts() < 4 {
639+
if client.Attempts() < client.NumResponses() {
638640
t.Fatalf("azure: DoPollForAsynchronous stopped polling before receiving a terminated OperationResource")
639641
}
640642

@@ -657,7 +659,7 @@ func TestDoPollForAsynchronous_PollsForStatusCreated(t *testing.T) {
657659
r, _ := autorest.SendWithSender(client, mocks.NewRequest(),
658660
DoPollForAsynchronous(time.Millisecond))
659661

660-
if client.Attempts() < 4 {
662+
if client.Attempts() < client.NumResponses() {
661663
t.Fatalf("azure: DoPollForAsynchronous stopped polling before receiving a terminated OperationResource")
662664
}
663665

@@ -681,7 +683,7 @@ func TestDoPollForAsynchronous_PollsUntilProvisioningStatusTerminates(t *testing
681683
r, _ := autorest.SendWithSender(client, mocks.NewRequest(),
682684
DoPollForAsynchronous(time.Millisecond))
683685

684-
if client.Attempts() < 4 {
686+
if client.Attempts() < client.NumResponses() {
685687
t.Fatalf("azure: DoPollForAsynchronous stopped polling before receiving a terminated OperationResource")
686688
}
687689

@@ -705,7 +707,7 @@ func TestDoPollForAsynchronous_PollsUntilProvisioningStatusSucceeds(t *testing.T
705707
r, _ := autorest.SendWithSender(client, mocks.NewRequest(),
706708
DoPollForAsynchronous(time.Millisecond))
707709

708-
if client.Attempts() < 4 {
710+
if client.Attempts() < client.NumResponses() {
709711
t.Fatalf("azure: DoPollForAsynchronous stopped polling before receiving a terminated OperationResource")
710712
}
711713

@@ -726,7 +728,7 @@ func TestDoPollForAsynchronous_PollsUntilOperationResourceHasTerminated(t *testi
726728
r, _ := autorest.SendWithSender(client, mocks.NewRequest(),
727729
DoPollForAsynchronous(time.Millisecond))
728730

729-
if client.Attempts() < 4 {
731+
if client.Attempts() < client.NumResponses() {
730732
t.Fatalf("azure: DoPollForAsynchronous stopped polling before receiving a terminated OperationResource")
731733
}
732734

@@ -747,7 +749,7 @@ func TestDoPollForAsynchronous_PollsUntilOperationResourceHasSucceeded(t *testin
747749
r, _ := autorest.SendWithSender(client, mocks.NewRequest(),
748750
DoPollForAsynchronous(time.Millisecond))
749751

750-
if client.Attempts() < 4 {
752+
if client.Attempts() < client.NumResponses() {
751753
t.Fatalf("azure: DoPollForAsynchronous stopped polling before receiving a terminated OperationResource")
752754
}
753755

@@ -876,7 +878,7 @@ func TestDoPollForAsynchronous_ReturnsErrorForLastErrorResponse(t *testing.T) {
876878
r1.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
877879
r2 := newProvisioningStatusResponse("busy")
878880
r2.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
879-
r3 := newAsynchronousResponseWithError()
881+
r3 := newAsynchronousResponseWithError("400 Bad Request", http.StatusBadRequest)
880882
r3.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
881883

882884
client := mocks.NewSender()
@@ -923,7 +925,7 @@ func TestDoPollForAsynchronous_ReturnsOperationResourceErrorForFailedOperations(
923925

924926
func TestDoPollForAsynchronous_ReturnsErrorForFirstPutRequest(t *testing.T) {
925927
// Return 400 bad response with error code and message in first put
926-
r1 := newAsynchronousResponseWithError()
928+
r1 := newAsynchronousResponseWithError("400 Bad Request", http.StatusBadRequest)
927929
client := mocks.NewSender()
928930
client.AppendResponse(r1)
929931

@@ -1044,7 +1046,7 @@ func TestFuture_PollsUntilProvisioningStatusSucceeds(t *testing.T) {
10441046
time.Sleep(delay)
10451047
}
10461048

1047-
if client.Attempts() < 4 {
1049+
if client.Attempts() < client.NumResponses() {
10481050
t.Fatalf("azure: TestFuture stopped polling before receiving a terminated OperationResource")
10491051
}
10501052

@@ -1096,6 +1098,129 @@ func TestFuture_Marshalling(t *testing.T) {
10961098
}
10971099
}
10981100

1101+
func TestFuture_WaitForCompletion(t *testing.T) {
1102+
r1 := newAsynchronousResponse()
1103+
r1.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
1104+
r2 := newProvisioningStatusResponse("busy")
1105+
r2.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
1106+
r3 := newAsynchronousResponseWithError("Internal server error", http.StatusInternalServerError)
1107+
r3.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
1108+
r3.Header.Del(http.CanonicalHeaderKey("Retry-After"))
1109+
r4 := newProvisioningStatusResponse(operationSucceeded)
1110+
r4.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
1111+
1112+
sender := mocks.NewSender()
1113+
sender.AppendResponse(r1)
1114+
sender.AppendError(errors.New("transient network failure"))
1115+
sender.AppendAndRepeatResponse(r2, 2)
1116+
sender.AppendResponse(r3)
1117+
sender.AppendResponse(r4)
1118+
1119+
future := NewFuture(mocks.NewRequest())
1120+
1121+
client := autorest.Client{
1122+
PollingDelay: 1 * time.Second,
1123+
PollingDuration: autorest.DefaultPollingDuration,
1124+
RetryAttempts: autorest.DefaultRetryAttempts,
1125+
RetryDuration: 1 * time.Second,
1126+
Sender: sender,
1127+
}
1128+
1129+
err := future.WaitForCompletion(context.Background(), client)
1130+
if err != nil {
1131+
t.Fatalf("azure: WaitForCompletion returned non-nil error")
1132+
}
1133+
1134+
if sender.Attempts() < sender.NumResponses() {
1135+
t.Fatalf("azure: TestFuture stopped polling before receiving a terminated OperationResource")
1136+
}
1137+
1138+
autorest.Respond(future.Response(),
1139+
autorest.ByClosing())
1140+
}
1141+
1142+
func TestFuture_WaitForCompletionTimedOut(t *testing.T) {
1143+
r1 := newAsynchronousResponse()
1144+
r1.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
1145+
r2 := newProvisioningStatusResponse("busy")
1146+
r2.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
1147+
1148+
sender := mocks.NewSender()
1149+
sender.AppendResponse(r1)
1150+
sender.AppendAndRepeatResponseWithDelay(r2, 1*time.Second, 5)
1151+
1152+
future := NewFuture(mocks.NewRequest())
1153+
1154+
client := autorest.Client{
1155+
PollingDelay: autorest.DefaultPollingDelay,
1156+
PollingDuration: 2 * time.Second,
1157+
RetryAttempts: autorest.DefaultRetryAttempts,
1158+
RetryDuration: 1 * time.Second,
1159+
Sender: sender,
1160+
}
1161+
1162+
err := future.WaitForCompletion(context.Background(), client)
1163+
if err == nil {
1164+
t.Fatalf("azure: WaitForCompletion returned nil error, should have timed out")
1165+
}
1166+
}
1167+
1168+
func TestFuture_WaitForCompletionRetriesExceeded(t *testing.T) {
1169+
r1 := newAsynchronousResponse()
1170+
r1.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
1171+
1172+
sender := mocks.NewSender()
1173+
sender.AppendResponse(r1)
1174+
sender.AppendAndRepeatError(errors.New("transient network failure"), autorest.DefaultRetryAttempts+1)
1175+
1176+
future := NewFuture(mocks.NewRequest())
1177+
1178+
client := autorest.Client{
1179+
PollingDelay: autorest.DefaultPollingDelay,
1180+
PollingDuration: autorest.DefaultPollingDuration,
1181+
RetryAttempts: autorest.DefaultRetryAttempts,
1182+
RetryDuration: 100 * time.Millisecond,
1183+
Sender: sender,
1184+
}
1185+
1186+
err := future.WaitForCompletion(context.Background(), client)
1187+
if err == nil {
1188+
t.Fatalf("azure: WaitForCompletion returned nil error, should have errored out")
1189+
}
1190+
}
1191+
1192+
func TestFuture_WaitForCompletionCancelled(t *testing.T) {
1193+
r1 := newAsynchronousResponse()
1194+
r1.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
1195+
r2 := newProvisioningStatusResponse("busy")
1196+
r2.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
1197+
1198+
sender := mocks.NewSender()
1199+
sender.AppendResponse(r1)
1200+
sender.AppendAndRepeatResponseWithDelay(r2, 1*time.Second, 5)
1201+
1202+
future := NewFuture(mocks.NewRequest())
1203+
1204+
client := autorest.Client{
1205+
PollingDelay: autorest.DefaultPollingDelay,
1206+
PollingDuration: autorest.DefaultPollingDuration,
1207+
RetryAttempts: autorest.DefaultRetryAttempts,
1208+
RetryDuration: autorest.DefaultRetryDuration,
1209+
Sender: sender,
1210+
}
1211+
1212+
ctx, cancel := context.WithCancel(context.Background())
1213+
go func() {
1214+
time.Sleep(2 * time.Second)
1215+
cancel()
1216+
}()
1217+
1218+
err := future.WaitForCompletion(ctx, client)
1219+
if err == nil {
1220+
t.Fatalf("azure: WaitForCompletion returned nil error, should have been cancelled")
1221+
}
1222+
}
1223+
10991224
const (
11001225
operationResourceIllegal = `
11011226
This is not JSON and should fail...badly.
@@ -1171,8 +1296,8 @@ func newAsynchronousResponse() *http.Response {
11711296
return r
11721297
}
11731298

1174-
func newAsynchronousResponseWithError() *http.Response {
1175-
r := mocks.NewResponseWithStatus("400 Bad Request", http.StatusBadRequest)
1299+
func newAsynchronousResponseWithError(response string, status int) *http.Response {
1300+
r := mocks.NewResponseWithStatus(response, status)
11761301
mocks.SetRetryHeader(r, retryDelay)
11771302
r.Request = mocks.NewRequestForURL(mocks.TestURL)
11781303
r.Body = mocks.NewBody(errorResponse)

autorest/client.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ const (
3535

3636
// DefaultRetryAttempts is number of attempts for retry status codes (5xx).
3737
DefaultRetryAttempts = 3
38+
39+
// DefaultRetryDuration is the duration to wait between retries.
40+
DefaultRetryDuration = 30 * time.Second
3841
)
3942

4043
var (
@@ -172,7 +175,7 @@ func NewClientWithUserAgent(ua string) Client {
172175
PollingDelay: DefaultPollingDelay,
173176
PollingDuration: DefaultPollingDuration,
174177
RetryAttempts: DefaultRetryAttempts,
175-
RetryDuration: 30 * time.Second,
178+
RetryDuration: DefaultRetryDuration,
176179
UserAgent: defaultUserAgent,
177180
}
178181
c.Sender = c.sender()

0 commit comments

Comments
 (0)