-
Notifications
You must be signed in to change notification settings - Fork 850
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Simplified implementation of LRO pollers for ARM. Public surface area has been slightly changed, making it identical to the data-plane implementation. The different polling mechanisms have been split into internal packages, with an exported LROPoller that implements the overall polling algorithm.
- Loading branch information
1 parent
8812d1a
commit f54f4fc
Showing
13 changed files
with
1,210 additions
and
961 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
// +build go1.13 | ||
|
||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
package async | ||
|
||
import ( | ||
"errors" | ||
"net/http" | ||
|
||
"github.com/Azure/azure-sdk-for-go/sdk/armcore/internal/pollers" | ||
"github.com/Azure/azure-sdk-for-go/sdk/azcore" | ||
) | ||
|
||
const ( | ||
finalStateAsync = "azure-async-operation" | ||
finalStateLoc = "location" | ||
finalStateOrig = "original-uri" | ||
) | ||
|
||
// Applicable returns true if the LRO is using Azure-AsyncOperation. | ||
func Applicable(resp *azcore.Response) bool { | ||
return resp.Header.Get(pollers.HeaderAzureAsync) != "" | ||
} | ||
|
||
// Poller is an LRO poller that uses the Azure-AsyncOperation pattern. | ||
type Poller struct { | ||
Type string `json:"type"` | ||
AsyncURL string `json:"asyncURL"` | ||
LocURL string `json:"locURL"` | ||
OrigURL string `json:"origURL"` | ||
Method string `json:"method"` | ||
FinalState string `json:"finalState"` | ||
CurState string `json:"state"` | ||
} | ||
|
||
// New creates a new Poller from the provided initial response and final-state type. | ||
func New(resp *azcore.Response, finalState string, pollerID string) (*Poller, error) { | ||
azcore.Log().Write(azcore.LogLongRunningOperation, "Using Azure-AsyncOperation poller.") | ||
asyncURL := resp.Header.Get(pollers.HeaderAzureAsync) | ||
if asyncURL == "" { | ||
return nil, errors.New("response is missing Azure-AsyncOperation header") | ||
} | ||
p := &Poller{ | ||
Type: pollers.MakeID(pollerID, "async"), | ||
AsyncURL: asyncURL, | ||
LocURL: resp.Header.Get(pollers.HeaderLocation), | ||
OrigURL: resp.Request.URL.String(), | ||
Method: resp.Request.Method, | ||
FinalState: finalState, | ||
} | ||
// check for provisioning state | ||
state, err := pollers.GetProvisioningState(resp) | ||
if errors.Is(err, pollers.ErrNoProvisioningState) { | ||
if resp.Request.Method == http.MethodPut { | ||
// initial response for a PUT requires a provisioning state | ||
return nil, err | ||
} | ||
// for DELETE/PATCH/POST, provisioning state is optional | ||
state = "InProgress" | ||
} else if err != nil { | ||
return nil, err | ||
} | ||
p.CurState = state | ||
return p, nil | ||
} | ||
|
||
// Done returns true if the LRO has reached a terminal state. | ||
func (p *Poller) Done() bool { | ||
return pollers.IsTerminalState(p.Status()) | ||
} | ||
|
||
// Update updates the Poller from the polling response. | ||
func (p *Poller) Update(resp *azcore.Response) error { | ||
state, err := pollers.GetStatus(resp) | ||
if err != nil { | ||
return err | ||
} | ||
p.CurState = state | ||
return nil | ||
} | ||
|
||
// FinalGetURL returns the URL to perform a final GET for the payload, or the empty string if not required. | ||
func (p *Poller) FinalGetURL() string { | ||
if p.Method == http.MethodPatch || p.Method == http.MethodPut { | ||
// for PATCH and PUT, the final GET is on the original resource URL | ||
return p.OrigURL | ||
} else if p.Method == http.MethodPost { | ||
// for POST, we need to consult the final-state-via flag | ||
if p.FinalState == finalStateLoc && p.LocURL != "" { | ||
return p.LocURL | ||
} else if p.FinalState == finalStateOrig { | ||
return p.OrigURL | ||
} | ||
// finalStateAsync fall through | ||
} | ||
return "" | ||
} | ||
|
||
// URL returns the polling URL. | ||
func (p *Poller) URL() string { | ||
return p.AsyncURL | ||
} | ||
|
||
// Status returns the status of the LRO. | ||
func (p *Poller) Status() string { | ||
return p.CurState | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
// +build go1.13 | ||
|
||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
package async | ||
|
||
import ( | ||
"io" | ||
"net/http" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/Azure/azure-sdk-for-go/sdk/armcore/internal/pollers" | ||
"github.com/Azure/azure-sdk-for-go/sdk/azcore" | ||
) | ||
|
||
const ( | ||
fakePollingURL = "https://foo.bar.baz/status" | ||
fakeResourceURL = "https://foo.bar.baz/resource" | ||
) | ||
|
||
func initialResponse(method string, resp io.Reader) *azcore.Response { | ||
req, err := http.NewRequest(method, fakeResourceURL, nil) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return &azcore.Response{ | ||
Response: &http.Response{ | ||
Body: io.NopCloser(resp), | ||
Header: http.Header{}, | ||
Request: req, | ||
}, | ||
} | ||
} | ||
|
||
func pollingResponse(resp io.Reader) *azcore.Response { | ||
return &azcore.Response{ | ||
Response: &http.Response{ | ||
Body: io.NopCloser(resp), | ||
Header: http.Header{}, | ||
}, | ||
} | ||
} | ||
|
||
func TestApplicable(t *testing.T) { | ||
resp := azcore.Response{ | ||
Response: &http.Response{ | ||
Header: http.Header{}, | ||
}, | ||
} | ||
if Applicable(&resp) { | ||
t.Fatal("missing Azure-AsyncOperation should not be applicable") | ||
} | ||
resp.Response.Header.Set(pollers.HeaderAzureAsync, fakePollingURL) | ||
if !Applicable(&resp) { | ||
t.Fatal("having Azure-AsyncOperation should be applicable") | ||
} | ||
} | ||
|
||
func TestNew(t *testing.T) { | ||
const jsonBody = `{ "properties": { "provisioningState": "Started" } }` | ||
resp := initialResponse(http.MethodPut, strings.NewReader(jsonBody)) | ||
resp.Header.Set(pollers.HeaderAzureAsync, fakePollingURL) | ||
poller, err := New(resp, "", "pollerID") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if poller.Done() { | ||
t.Fatal("poller should not be done") | ||
} | ||
if u := poller.FinalGetURL(); u != fakeResourceURL { | ||
t.Fatalf("unexpected final get URL %s", u) | ||
} | ||
if s := poller.Status(); s != "Started" { | ||
t.Fatalf("unexpected status %s", s) | ||
} | ||
if u := poller.URL(); u != fakePollingURL { | ||
t.Fatalf("unexpected polling URL %s", u) | ||
} | ||
if err := poller.Update(pollingResponse(strings.NewReader(`{ "status": "InProgress" }`))); err != nil { | ||
t.Fatal(err) | ||
} | ||
if s := poller.Status(); s != "InProgress" { | ||
t.Fatalf("unexpected status %s", s) | ||
} | ||
} | ||
|
||
func TestNewDeleteNoProvState(t *testing.T) { | ||
resp := initialResponse(http.MethodDelete, http.NoBody) | ||
resp.Header.Set(pollers.HeaderAzureAsync, fakePollingURL) | ||
poller, err := New(resp, "", "pollerID") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if poller.Done() { | ||
t.Fatal("poller should not be done") | ||
} | ||
if s := poller.Status(); s != "InProgress" { | ||
t.Fatalf("unexpected status %s", s) | ||
} | ||
} | ||
|
||
func TestNewFail(t *testing.T) { | ||
// missing provisioning state on initial response | ||
resp := initialResponse(http.MethodPut, http.NoBody) | ||
resp.Header.Set(pollers.HeaderAzureAsync, fakePollingURL) | ||
poller, err := New(resp, "", "pollerID") | ||
if err == nil { | ||
t.Fatal("unexpected nil error") | ||
} | ||
if poller != nil { | ||
t.Fatal("expected nil poller") | ||
} | ||
} | ||
|
||
func TestNewFinalGetLocation(t *testing.T) { | ||
const ( | ||
jsonBody = `{ "properties": { "provisioningState": "Started" } }` | ||
locURL = "https://foo.bar.baz/location" | ||
) | ||
resp := initialResponse(http.MethodPost, strings.NewReader(jsonBody)) | ||
resp.Header.Set(pollers.HeaderAzureAsync, fakePollingURL) | ||
resp.Header.Set(pollers.HeaderLocation, locURL) | ||
poller, err := New(resp, "location", "pollerID") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if poller.Done() { | ||
t.Fatal("poller should not be done") | ||
} | ||
if u := poller.FinalGetURL(); u != locURL { | ||
t.Fatalf("unexpected final get URL %s", u) | ||
} | ||
if u := poller.URL(); u != fakePollingURL { | ||
t.Fatalf("unexpected polling URL %s", u) | ||
} | ||
} | ||
|
||
func TestNewFinalGetOrigin(t *testing.T) { | ||
const ( | ||
jsonBody = `{ "properties": { "provisioningState": "Started" } }` | ||
locURL = "https://foo.bar.baz/location" | ||
) | ||
resp := initialResponse(http.MethodPost, strings.NewReader(jsonBody)) | ||
resp.Header.Set(pollers.HeaderAzureAsync, fakePollingURL) | ||
resp.Header.Set(pollers.HeaderLocation, locURL) | ||
poller, err := New(resp, "original-uri", "pollerID") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if poller.Done() { | ||
t.Fatal("poller should not be done") | ||
} | ||
if u := poller.FinalGetURL(); u != fakeResourceURL { | ||
t.Fatalf("unexpected final get URL %s", u) | ||
} | ||
if u := poller.URL(); u != fakePollingURL { | ||
t.Fatalf("unexpected polling URL %s", u) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
// +build go1.13 | ||
|
||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
package body | ||
|
||
import ( | ||
"errors" | ||
"net/http" | ||
|
||
"github.com/Azure/azure-sdk-for-go/sdk/armcore/internal/pollers" | ||
"github.com/Azure/azure-sdk-for-go/sdk/azcore" | ||
) | ||
|
||
// Applicable returns true if the LRO is using no headers, just provisioning state. | ||
// This is only applicable to PATCH and PUT methods and assumes no polling headers. | ||
func Applicable(resp *azcore.Response) bool { | ||
// we can't check for absense of headers due to some misbehaving services | ||
// like redis that return a Location header but don't actually use that protocol | ||
return resp.Request.Method == http.MethodPatch || resp.Request.Method == http.MethodPut | ||
} | ||
|
||
// Poller is an LRO poller that uses the Body pattern. | ||
type Poller struct { | ||
Type string `json:"type"` | ||
PollURL string `json:"pollURL"` | ||
CurState string `json:"state"` | ||
} | ||
|
||
// New creates a new Poller from the provided initial response. | ||
func New(resp *azcore.Response, pollerID string) (*Poller, error) { | ||
azcore.Log().Write(azcore.LogLongRunningOperation, "Using Body poller.") | ||
p := &Poller{ | ||
Type: pollers.MakeID(pollerID, "body"), | ||
PollURL: resp.Request.URL.String(), | ||
} | ||
// the initial response must contain a provisioning state | ||
state, err := pollers.GetProvisioningState(resp) | ||
if err != nil { | ||
return nil, err | ||
} | ||
p.CurState = state | ||
return p, nil | ||
} | ||
|
||
// URL returns the polling URL. | ||
func (p *Poller) URL() string { | ||
return p.PollURL | ||
} | ||
|
||
// Done returns true if the LRO has reached a terminal state. | ||
func (p *Poller) Done() bool { | ||
return pollers.IsTerminalState(p.Status()) | ||
} | ||
|
||
// Update updates the Poller from the polling response. | ||
func (p *Poller) Update(resp *azcore.Response) error { | ||
state, err := pollers.GetProvisioningState(resp) | ||
if errors.Is(err, pollers.ErrNoProvisioningState) { | ||
// absense of any provisioning state is considered terminal success | ||
state = "Succeeded" | ||
} else if err != nil { | ||
return err | ||
} | ||
p.CurState = state | ||
return nil | ||
} | ||
|
||
// FinalGetURL returns the empty string as no final GET is required for this poller type. | ||
func (*Poller) FinalGetURL() string { | ||
return "" | ||
} | ||
|
||
// Status returns the status of the LRO. | ||
func (p *Poller) Status() string { | ||
return p.CurState | ||
} |
Oops, something went wrong.