Skip to content

Commit 3ed9334

Browse files
authored
Merge pull request #1108 from hashicorp/b/broader-tcp-dial-failure-handling
Error Handling - update connection timeout regex to catch broader range of error conditions
2 parents 2257d18 + 6b80cbf commit 3ed9334

File tree

5 files changed

+66
-30
lines changed

5 files changed

+66
-30
lines changed

sdk/client/client.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -777,10 +777,10 @@ func extendedRetryPolicy(resp *http.Response, err error) (bool, error) {
777777

778778
// A regular expression to catch dial timeouts in the underlying TCP session
779779
// connection
780-
tcpDialTimeoutRe := regexp.MustCompile(`dial tcp .*: i/o timeout`)
780+
tcpDialTCPRe := regexp.MustCompile(`dial tcp`)
781781

782-
// A regular expression to match complete packet loss - see comment below on packet-loss scenarios
783-
// completePacketLossRe := regexp.MustCompile(`EOF$`)
782+
// A regular expression to match complete packet loss
783+
completePacketLossRe := regexp.MustCompile(`EOF`)
784784

785785
if err != nil {
786786
var v *url.Error
@@ -805,15 +805,13 @@ func extendedRetryPolicy(resp *http.Response, err error) (bool, error) {
805805
return false, v
806806
}
807807

808-
if tcpDialTimeoutRe.MatchString(v.Error()) {
808+
if tcpDialTCPRe.MatchString(v.Error()) {
809809
return false, v
810810
}
811811

812-
// TODO - Need to investigate how to deal with total packet-loss situations that doesn't break LRO retries.
813-
// Such as Temporary Proxy outage, or recoverable disruption to network traffic (e.g. bgp events etc)
814-
// if completePacketLossRe.MatchString(v.Error()) {
815-
// return false, v
816-
// }
812+
if completePacketLossRe.MatchString(v.Error()) {
813+
return false, v
814+
}
817815

818816
var certificateVerificationError *tls.CertificateVerificationError
819817
if ok := errors.As(v.Err, &certificateVerificationError); ok {

sdk/client/resourcemanager/poller_lro.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"bytes"
88
"context"
99
"encoding/json"
10+
"errors"
1011
"fmt"
1112
"io"
1213
"net/http"
@@ -27,6 +28,9 @@ type longRunningOperationPoller struct {
2728
initialRetryDuration time.Duration
2829
originalUrl *url.URL
2930
pollingUrl *url.URL
31+
32+
droppedConnectionCount int
33+
maxDroppedConnections int
3034
}
3135

3236
func pollingUriForLongRunningOperation(resp *client.Response) string {
@@ -39,8 +43,9 @@ func pollingUriForLongRunningOperation(resp *client.Response) string {
3943

4044
func longRunningOperationPollerFromResponse(resp *client.Response, client *client.Client) (*longRunningOperationPoller, error) {
4145
poller := longRunningOperationPoller{
42-
client: client,
43-
initialRetryDuration: 10 * time.Second,
46+
client: client,
47+
initialRetryDuration: 10 * time.Second,
48+
maxDroppedConnections: 3,
4449
}
4550

4651
pollingUrl := pollingUriForLongRunningOperation(resp)
@@ -107,9 +112,20 @@ func (p *longRunningOperationPoller) Poll(ctx context.Context) (result *pollers.
107112
}
108113
result.HttpResponse, err = req.Execute(ctx)
109114
if err != nil {
115+
var e *url.Error
116+
if errors.As(err, &e) {
117+
p.droppedConnectionCount++
118+
if p.droppedConnectionCount < p.maxDroppedConnections {
119+
result.Status = pollers.PollingStatusUnknown
120+
return result, nil
121+
}
122+
}
123+
110124
return nil, err
111125
}
112126

127+
p.droppedConnectionCount = 0
128+
113129
if result.HttpResponse != nil {
114130
var respBody []byte
115131
respBody, err = io.ReadAll(result.HttpResponse.Body)

sdk/client/resourcemanager/poller_lro_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ func TestPollerLRO_InProvisioningState_AcceptedThenDroppedThenInProgressThenSucc
326326

327327
expectedStatuses := []pollers.PollingStatus{
328328
pollers.PollingStatusInProgress, // the 202 Accepted
329+
pollers.PollingStatusUnknown,
329330
// NOTE: the Dropped Connection will be ignored/silently retried
330331
pollers.PollingStatusInProgress, // working on it
331332
pollers.PollingStatusSucceeded, // good
@@ -366,7 +367,7 @@ func TestPollerLRO_InStatus_AcceptedThenDroppedThenInProgressThenSuccess(t *test
366367

367368
expectedStatuses := []pollers.PollingStatus{
368369
pollers.PollingStatusInProgress, // the 202 Accepted
369-
// NOTE: the Dropped Connection will be ignored/silently retried
370+
pollers.PollingStatusUnknown,
370371
pollers.PollingStatusInProgress, // working on it
371372
pollers.PollingStatusSucceeded, // good
372373
}

sdk/client/resourcemanager/poller_provisioning_state.go

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package resourcemanager
55

66
import (
77
"context"
8+
"errors"
89
"fmt"
910
"net/http"
1011
"net/url"
@@ -27,6 +28,9 @@ type provisioningStatePoller struct {
2728
initialRetryDuration time.Duration
2829
originalUri string
2930
resourcePath string
31+
32+
droppedConnectionCount int
33+
maxDroppedConnections int
3034
}
3135

3236
func provisioningStatePollerFromResponse(response *client.Response, lroIsSelfReference bool, client *Client, pollingInterval time.Duration) (*provisioningStatePoller, error) {
@@ -58,11 +62,12 @@ func provisioningStatePollerFromResponse(response *client.Response, lroIsSelfRef
5862
}
5963

6064
return &provisioningStatePoller{
61-
apiVersion: apiVersion,
62-
client: client,
63-
initialRetryDuration: pollingInterval,
64-
originalUri: originalUri,
65-
resourcePath: resourcePath,
65+
apiVersion: apiVersion,
66+
client: client,
67+
initialRetryDuration: pollingInterval,
68+
originalUri: originalUri,
69+
resourcePath: resourcePath,
70+
maxDroppedConnections: 3,
6671
}, nil
6772
}
6873

@@ -84,8 +89,22 @@ func (p *provisioningStatePoller) Poll(ctx context.Context) (*pollers.PollResult
8489
}
8590
resp, err := p.client.Execute(ctx, req)
8691
if err != nil {
92+
var e *url.Error
93+
if errors.As(err, &e) {
94+
p.droppedConnectionCount++
95+
if p.droppedConnectionCount < p.maxDroppedConnections {
96+
return &pollers.PollResult{
97+
PollInterval: p.initialRetryDuration,
98+
Status: pollers.PollingStatusUnknown,
99+
}, nil
100+
}
101+
}
102+
87103
return nil, fmt.Errorf("executing request: %+v", err)
88104
}
105+
106+
p.droppedConnectionCount = 0
107+
89108
if resp == nil {
90109
return nil, pollers.PollingDroppedConnectionError{}
91110
}

sdk/client/resourcemanager/poller_provisioning_state_test.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ func TestPollerProvisioningState_OkWithNoBody_AfterPolling(t *testing.T) {
157157
}
158158

159159
func TestPollerProvisioningState_InProvisioningState_DroppedThenInProgressThenSuccess(t *testing.T) {
160-
ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second)
160+
ctx, cancel := context.WithTimeout(context.TODO(), 90*time.Second)
161161
defer cancel()
162162

163163
// changing where we're setting this for the heck of it
@@ -177,15 +177,17 @@ func TestPollerProvisioningState_InProvisioningState_DroppedThenInProgressThenSu
177177
apiVersion: "2020-01-01",
178178
}
179179
poller := provisioningStatePoller{
180-
apiVersion: helper.expectedApiVersion,
181-
client: resourceManagerClient,
182-
initialRetryDuration: 10,
183-
originalUri: "/provisioning-state/poll",
184-
resourcePath: "/provisioning-state/poll",
180+
apiVersion: helper.expectedApiVersion,
181+
client: resourceManagerClient,
182+
initialRetryDuration: 10,
183+
originalUri: "/provisioning-state/poll",
184+
resourcePath: "/provisioning-state/poll",
185+
maxDroppedConnections: 3,
185186
}
186187

187188
expectedStatuses := []pollers.PollingStatus{
188189
pollers.PollingStatusInProgress, // working on it
190+
pollers.PollingStatusUnknown,
189191
// NOTE: the Dropped Connection will be ignored/silently retried
190192
pollers.PollingStatusInProgress, // working on it
191193
pollers.PollingStatusSucceeded, // good
@@ -200,7 +202,6 @@ func TestPollerProvisioningState_InProvisioningState_DroppedThenInProgressThenSu
200202
t.Fatalf("expected status to be %q but got %q", expected, result.Status)
201203
}
202204
}
203-
// sanity-checking - expect 4 calls but 3 statuses (since the dropped connection is silently retried)
204205
helper.assertCalled(t, 4)
205206
}
206207

@@ -225,16 +226,17 @@ func TestPollerProvisioningState_InStatus_DroppedThenInProgressThenSuccess(t *te
225226
apiVersion: "2020-01-01",
226227
}
227228
poller := provisioningStatePoller{
228-
apiVersion: helper.expectedApiVersion,
229-
client: resourceManagerClient,
230-
initialRetryDuration: 10,
231-
originalUri: "/provisioning-state/poll",
232-
resourcePath: "/provisioning-state/poll",
229+
apiVersion: helper.expectedApiVersion,
230+
client: resourceManagerClient,
231+
initialRetryDuration: 10,
232+
originalUri: "/provisioning-state/poll",
233+
resourcePath: "/provisioning-state/poll",
234+
maxDroppedConnections: 3,
233235
}
234236

235237
expectedStatuses := []pollers.PollingStatus{
236238
pollers.PollingStatusInProgress, // working on it
237-
// NOTE: the Dropped Connection will be ignored/silently retried
239+
pollers.PollingStatusUnknown,
238240
pollers.PollingStatusInProgress, // working on it
239241
pollers.PollingStatusSucceeded, // good
240242
}

0 commit comments

Comments
 (0)