Skip to content

Commit

Permalink
LRO poller rewrite
Browse files Browse the repository at this point in the history
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
jhendrixMSFT committed Jun 7, 2021
1 parent 8812d1a commit ced6be7
Show file tree
Hide file tree
Showing 13 changed files with 1,210 additions and 961 deletions.
4 changes: 2 additions & 2 deletions sdk/armcore/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ module github.com/Azure/azure-sdk-for-go/sdk/armcore
go 1.14

require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.14.0
github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.16.2
github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.1
)
9 changes: 4 additions & 5 deletions sdk/armcore/go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.14.0 h1:4HBTI/9UDZN7tsXyB5TYP3xCv5xVHIUTbvHHH2HFxQY=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.14.0/go.mod h1:pElNP+u99BvCZD+0jOlhI9OC/NB2IDTOTGZOZH0Qhq8=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.0 h1:HG1ggl8L3ZkV/Ydanf7lKr5kkhhPGCpWdnr1J6v7cO4=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.0/go.mod h1:k4KbFSunV/+0hOHL1vyFaPsiYQ1Vmvy1TBpmtvCDLZM=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.16.2 h1:UC4vfOhW2l0f2QOCQpOxJS4/K6oKFy2tQZE+uWU1MEo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.16.2/go.mod h1:MVdrcUC4Hup35qHym3VdzoW+NBgBxrta9Vei97jRtM8=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.1 h1:vx8McI56N5oLSQu8xa+xdiE0fjQq8W8Zt49vHP8Rygw=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.1/go.mod h1:k4KbFSunV/+0hOHL1vyFaPsiYQ1Vmvy1TBpmtvCDLZM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
Expand All @@ -10,7 +10,6 @@ golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
Expand Down
109 changes: 109 additions & 0 deletions sdk/armcore/internal/pollers/async/async.go
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
}
161 changes: 161 additions & 0 deletions sdk/armcore/internal/pollers/async/async_test.go
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)
}
}
78 changes: 78 additions & 0 deletions sdk/armcore/internal/pollers/body/body.go
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
}
Loading

0 comments on commit ced6be7

Please sign in to comment.