Merge lp:~jtv/gwacl/batch-list-containers into lp:gwacl

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: 102
Merged at revision: 96
Proposed branch: lp:~jtv/gwacl/batch-list-containers
Merge into: lp:gwacl
Diff against target: 329 lines (+135/-30)
5 files modified
Makefile (+0/-2)
storage_base.go (+53/-11)
storage_base_test.go (+56/-10)
testhelpers_http.go (+25/-4)
testhelpers_misc.go (+1/-3)
To merge this branch: bzr merge lp:~jtv/gwacl/batch-list-containers
Reviewer Review Type Date Requested Status
Raphaël Badin (community) Approve
Review via email: mp+160844@code.launchpad.net

Commit message

Support batched results in ListContainers().

Description of the change

This is actually required for ListContainers() to function properly. I kept the existing tests, but removed a NextMarker in an existing test. That test value was inappropriate (it was an unbatched result, so should not have a NextMarker) and broke the test once I had batch support working. Plus of course I added a test for a batching call before actually implementing support.

The code I ended up with isn't as simple as I'd like. If this were python, I'd test it much more thoroughly.

You'll also note some small drive-by improvements and TODO comments. In the Makefile you'll see that gofmt with the -s (simplify) option also does the regular work of a gofmt without the -s.

In a next branch I'll be doing the exact same batching for the List Blobs call. Hopefully I'll be able to extract some shared code. These were the only two instances of batching that I found in the storage API insofar as we support it.

Jeroen

To post a comment you must log in.
Revision history for this message
Raphaël Badin (rvb) wrote :

Looks good. I've got a few remarks but I'm not really familiar with that code so take them with a grain of salt.

[0]

[in ListContainers]
54 + firstBatch, marker, err := context.getListContainersBatch("")
55 if err != nil {
[...]
62 + return firstBatch, err
63 + }

Is it worth returning the firstBatch if we get an error here? Unless there is a good reason why we would want to do this (and then it's worth adding to the method's documentation), it would seem more logical to me to return "nil, err" in this case…

[1]

65 + batch, nextMarker, err := context.getListContainersBatch(marker)
66 + if err != nil {
67 + return firstBatch, err
68 + }

(let's forget [0] for a moment here) Why returning firstBatch here? To be more coherent with the first error return clause (the one which I talk about in [0]), I'd return the accumulated batches returned so far, *including the one we just got*. But maybe (see [0]) it's even more simple to simply return "nil, err".

[2]

77 + // Since we must have arrived at the last batch, where NextMarker is empty,
78 + // it would be misleading to report the original NextMarker.
79 + firstBatch.NextMarker = ""

I don't really understand the necessity for this (but I'm not really familiar with the context). From the looks of it, you're just introducing a small difference here, compared to what one will expect to get by reading the API documentation.

[3]

I understand the necessity for the nextMarker/marker trick given the way you've done things here… but how about a simpler implementation… something like this:
The method: http://paste.ubuntu.com/5600649/
The diff: http://paste.ubuntu.com/5600647/

review: Approve
lp:~jtv/gwacl/batch-list-containers updated
101. By Jeroen T. Vermeulen

Review suggestion: don't try to relay unreliable results in the error case.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (4.3 KiB)

> Looks good. I've got a few remarks but I'm not really familiar with that code
> so take them with a grain of salt.

Thanks for the review. Yes, the code is not side we worked on in Brisbane... What I can tell you is it gets a bit ugly sometimes. :)

> [0]
>
> [in ListContainers]
> 54 + firstBatch, marker, err := context.getListContainersBatch("")
> 55 if err != nil {
> [...]
> 62 + return firstBatch, err
> 63 + }
>
> Is it worth returning the firstBatch if we get an error here? Unless there is
> a good reason why we would want to do this (and then it's worth adding to the
> method's documentation), it would seem more logical to me to return "nil, err"
> in this case…

You're right. I changed it.

I was reasoning that if I pass on whatever we get, I don't have to worry about any possibility of getting a useful result despite an error: if that ever happened we could just document whatever Azure does and our existing code would probably support it correctly. But that doesn't work very well in the presence of batched results: what would we do if we got an error code but a usable result *with* a NextMarker? Defensive coding could cope with any sensible scenario, but absent a good indication that there's any point, it's not the right thing to do.

> [1]
>
> 65 + batch, nextMarker, err :=
> context.getListContainersBatch(marker)
> 66 + if err != nil {
> 67 + return firstBatch, err
> 68 + }
>
> (let's forget [0] for a moment here) Why returning firstBatch here? To be
> more coherent with the first error return clause (the one which I talk about
> in [0]), I'd return the accumulated batches returned so far, *including the
> one we just got*. But maybe (see [0]) it's even more simple to simply return
> "nil, err".

I returned nil here as well.

> [2]
>
> 77 + // Since we must have arrived at the last batch, where NextMarker
> is empty,
> 78 + // it would be misleading to report the original NextMarker.
> 79 + firstBatch.NextMarker = ""
>
> I don't really understand the necessity for this (but I'm not really familiar
> with the context). From the looks of it, you're just introducing a small
> difference here, compared to what one will expect to get by reading the API
> documentation.

It's not so much a matter of necessity, as of avoiding a misrepresentation. If NextMarker is nonempty, that means the result is incomplete. Thus, for the complete result, it would be improper to keep NextMarker set to a nonempty value. The way I did it, a user who expects ListContainers to correspond exactly to one call to the List Containers API method will not be misled. It's just that the default batch size will be effectively infinite. (Except of course Go will break if you try to make an array of 2³¹ entries, but they've been hinting that they'll fix that at some point.)

There are different ways to argue this. The root cause is that ListContainers no longer corresponds exactly to an API call: it operates at a slightly higher level. And so ContainerEnumerationResults now serves two purposes that are no longer fully aligned:

1....

Read more...

lp:~jtv/gwacl/batch-list-containers updated
102. By Jeroen T. Vermeulen

Streamline the ListContainers loop.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

Did you check to see if the example still runs after this change?

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

No, I didn't check... Couldn't, what with the consequences of that disk-deletion bug leaving my account "full." And indeed I left out a crucial bit, as corrected in my other branch.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'Makefile'
--- Makefile 2013-04-24 08:36:17 +0000
+++ Makefile 2013-04-25 14:03:27 +0000
@@ -11,9 +11,7 @@
1111
12# Reformat the source files to match our layout standards.12# Reformat the source files to match our layout standards.
13# This includes gofmt's "simplify" option to streamline the source code.13# This includes gofmt's "simplify" option to streamline the source code.
14# TODO: The -s run may actually also do the regular formatting.
15format:14format:
16 ./utilities/format
17 ./utilities/format -s15 ./utilities/format -s
1816
19# Build the examples (we have no tests for them).17# Build the examples (we have no tests for them).
2018
=== modified file 'storage_base.go'
--- storage_base.go 2013-04-22 16:00:31 +0000
+++ storage_base.go 2013-04-25 14:03:27 +0000
@@ -265,19 +265,61 @@
265 return resp, nil265 return resp, nil
266}266}
267267
268// Send a request to the storage service to list the containers in the268// getListContainersBatch calls the "List Containers" operation on the storage
269// storage account. error is non-nil if an error occurred.269// API, and returns a single batch of results; its "next marker" for batching,
270// and an error code.
271// The marker argument should be empty for a new List Containers request. for
272// subsequent calls to get additional batches of the same result, pass the
273// "next marker" returned by the previous call.
274// The "next marker" will be empty on the last batch.
275func (context *StorageContext) getListContainersBatch(marker string) (*ContainerEnumerationResults, string, error) {
276 uri := interpolateURL(
277 "http://___.blob.core.windows.net/?comp=list", context.Account)
278 req, err := http.NewRequest("GET", uri, nil)
279 if err != nil {
280 return nil, "", err
281 }
282 addStandardHeaders(req, context.Account, context.Key, "2012-02-12")
283 containers := ContainerEnumerationResults{}
284 _, err = context.send(req, &containers, http.StatusOK)
285 if err != nil {
286 return nil, "", err
287 }
288
289 // The response may contain a NextMarker field, to let us request a
290 // subsequent batch of results. The XML parser won't trim whitespace out
291 // of the marker tag, so we do that here.
292 nextMarker := strings.TrimSpace(containers.NextMarker)
293 return &containers, nextMarker, nil
294}
295
296// ListContainers sends a request to the storage service to list the containers
297// in the storage account. error is non-nil if an error occurred.
270func (context *StorageContext) ListContainers() (*ContainerEnumerationResults, error) {298func (context *StorageContext) ListContainers() (*ContainerEnumerationResults, error) {
271 uri := interpolateURL(299 containers := make([]Container, 0)
272 "http://___.blob.core.windows.net/?comp=list", context.Account)300 var batch *ContainerEnumerationResults
273 req, err := http.NewRequest("GET", uri, nil)301
274 if err != nil {302 // Request the initial result, using the empty marker. Then, for as long
275 return nil, err303 // as the result has a nonempty NextMarker, request the next batch using
304 // that marker.
305 for marker, nextMarker := "", "x"; nextMarker != ""; marker = nextMarker {
306 var err error
307 // Don't use := here or you'll shadow variables from the function's
308 // outer scopes.
309 batch, nextMarker, err = context.getListContainersBatch(marker)
310 if err != nil {
311 return nil, err
312 }
313 containers = append(containers, batch.Containers...)
276 }314 }
277 addStandardHeaders(req, context.Account, context.Key, "2012-02-12")315
278 containers := &ContainerEnumerationResults{}316 // There's more in a ContainerEnumerationResults than just the containers.
279 _, e := context.send(req, containers, http.StatusOK)317 // Return the latest batch, but give it the full cumulative containers list
280 return containers, e318 // instead of just the last batch.
319 // To the caller, this will look like they made one call to Azure's
320 // List Containers method, but batch size was unlimited.
321 batch.Containers = containers
322 return batch, nil
281}323}
282324
283// Send a request to the storage service to list the blobs in a container.325// Send a request to the storage service to list the blobs in a container.
284326
=== modified file 'storage_base_test.go'
--- storage_base_test.go 2013-04-24 10:08:54 +0000
+++ storage_base_test.go 2013-04-25 14:03:27 +0000
@@ -295,13 +295,13 @@
295 </Metadata>295 </Metadata>
296 </Container>296 </Container>
297 </Containers>297 </Containers>
298 <NextMarker>next-marker-value</NextMarker>298 <NextMarker/>
299 </EnumerationResults>`299 </EnumerationResults>`
300 context := makeStorageContext()300 context := makeStorageContext()
301 response := &http.Response{301 response := &http.Response{
302 Status: fmt.Sprintf("%d", http.StatusOK),302 Status: fmt.Sprintf("%d", http.StatusOK),
303 StatusCode: http.StatusOK,303 StatusCode: http.StatusOK,
304 Body: ioutil.NopCloser(strings.NewReader(response_body)),304 Body: makeResponseBody(response_body),
305 }305 }
306 transport := &TestTransport{Response: response}306 transport := &TestTransport{Response: response}
307 context.client = &http.Client{Transport: transport}307 context.client = &http.Client{Transport: transport}
@@ -337,6 +337,52 @@
337 c.Assert(err, NotNil)337 c.Assert(err, NotNil)
338}338}
339339
340// ListContainers combines multiple batches of output.
341func (suite *TestListContainers) TestBatchedResult(c *C) {
342 firstContainer := "container1"
343 lastContainer := "container2"
344 marker := "moreplease"
345 firstBatch := http.Response{
346 StatusCode: http.StatusOK,
347 Body: makeResponseBody(fmt.Sprintf(`
348 <EnumerationResults>
349 <Containers>
350 <Container>
351 <Name>%s</Name>
352 <URL>container-address</URL>
353 </Container>
354 </Containers>
355 <NextMarker>%s</NextMarker>
356 </EnumerationResults>
357 `, firstContainer, marker)),
358 }
359 lastBatch := http.Response{
360 StatusCode: http.StatusOK,
361 Body: makeResponseBody(fmt.Sprintf(`
362 <EnumerationResults>
363 <Containers>
364 <Container>
365 <Name>%s</Name>
366 <URL>container-address</URL>
367 </Container>
368 </Containers>
369 </EnumerationResults>
370 `, lastContainer)),
371 }
372 transport := TestTransport2{}
373 transport.AddExchange(&firstBatch, nil)
374 transport.AddExchange(&lastBatch, nil)
375 context := makeStorageContext()
376 context.client = &http.Client{Transport: &transport}
377
378 containers, err := context.ListContainers()
379 c.Assert(err, IsNil)
380
381 c.Check(len(containers.Containers), Equals, 2)
382 c.Check(containers.Containers[0].Name, Equals, firstContainer)
383 c.Check(containers.Containers[1].Name, Equals, lastContainer)
384}
385
340type TestListBlobs struct{}386type TestListBlobs struct{}
341387
342var _ = Suite(&TestListBlobs{})388var _ = Suite(&TestListBlobs{})
@@ -392,7 +438,7 @@
392 response := &http.Response{438 response := &http.Response{
393 Status: fmt.Sprintf("%d", http.StatusOK),439 Status: fmt.Sprintf("%d", http.StatusOK),
394 StatusCode: http.StatusOK,440 StatusCode: http.StatusOK,
395 Body: ioutil.NopCloser(strings.NewReader(response_body)),441 Body: makeResponseBody(response_body),
396 }442 }
397 transport := &TestTransport{Response: response}443 transport := &TestTransport{Response: response}
398 context.client = &http.Client{Transport: transport}444 context.client = &http.Client{Transport: transport}
@@ -523,7 +569,7 @@
523 response := &http.Response{569 response := &http.Response{
524 Status: "102 Frotzed",570 Status: "102 Frotzed",
525 StatusCode: 102,571 StatusCode: 102,
526 Body: ioutil.NopCloser(strings.NewReader("<Error><Code>Frotzed</Code><Message>failed to put blob</Message></Error>")),572 Body: makeResponseBody("<Error><Code>Frotzed</Code><Message>failed to put blob</Message></Error>"),
527 }573 }
528 transport := &TestTransport{Response: response}574 transport := &TestTransport{Response: response}
529 context := makeStorageContext()575 context := makeStorageContext()
@@ -582,7 +628,7 @@
582 response := &http.Response{628 response := &http.Response{
583 Status: "102 Frotzed",629 Status: "102 Frotzed",
584 StatusCode: 102,630 StatusCode: 102,
585 Body: ioutil.NopCloser(strings.NewReader("<Error><Code>Frotzed</Code><Message>failed to put block</Message></Error>")),631 Body: makeResponseBody("<Error><Code>Frotzed</Code><Message>failed to put block</Message></Error>"),
586 }632 }
587 transport := &TestTransport{Response: response}633 transport := &TestTransport{Response: response}
588 context := makeStorageContext()634 context := makeStorageContext()
@@ -645,7 +691,7 @@
645 response := &http.Response{691 response := &http.Response{
646 Status: "102 Frotzed",692 Status: "102 Frotzed",
647 StatusCode: 102,693 StatusCode: 102,
648 Body: ioutil.NopCloser(strings.NewReader("<Error><Code>Frotzed</Code><Message>failed to put blocklist</Message></Error>")),694 Body: makeResponseBody("<Error><Code>Frotzed</Code><Message>failed to put blocklist</Message></Error>"),
649 }695 }
650 transport := &TestTransport{Response: response}696 transport := &TestTransport{Response: response}
651 context := makeStorageContext()697 context := makeStorageContext()
@@ -686,7 +732,7 @@
686 response := &http.Response{732 response := &http.Response{
687 Status: fmt.Sprintf("%d", http.StatusOK),733 Status: fmt.Sprintf("%d", http.StatusOK),
688 StatusCode: http.StatusOK,734 StatusCode: http.StatusOK,
689 Body: ioutil.NopCloser(strings.NewReader(response_body)),735 Body: makeResponseBody(response_body),
690 }736 }
691 transport := &TestTransport{Response: response}737 transport := &TestTransport{Response: response}
692 context.client = &http.Client{Transport: transport}738 context.client = &http.Client{Transport: transport}
@@ -760,7 +806,7 @@
760 response := &http.Response{806 response := &http.Response{
761 Status: "146 Frotzed",807 Status: "146 Frotzed",
762 StatusCode: 146,808 StatusCode: 146,
763 Body: ioutil.NopCloser(strings.NewReader("<Error><Code>Frotzed</Code><Message>failed to delete blob</Message></Error>")),809 Body: makeResponseBody("<Error><Code>Frotzed</Code><Message>failed to delete blob</Message></Error>"),
764 }810 }
765 transport := &TestTransport{Response: response}811 transport := &TestTransport{Response: response}
766 context := makeStorageContext()812 context := makeStorageContext()
@@ -782,7 +828,7 @@
782 response := &http.Response{828 response := &http.Response{
783 Status: fmt.Sprintf("%d", http.StatusOK),829 Status: fmt.Sprintf("%d", http.StatusOK),
784 StatusCode: http.StatusOK,830 StatusCode: http.StatusOK,
785 Body: ioutil.NopCloser(strings.NewReader(response_body)),831 Body: makeResponseBody(response_body),
786 }832 }
787 transport := &TestTransport{Response: response}833 transport := &TestTransport{Response: response}
788 context.client = &http.Client{Transport: transport}834 context.client = &http.Client{Transport: transport}
@@ -818,7 +864,7 @@
818 response := &http.Response{864 response := &http.Response{
819 Status: "246 Frotzed",865 Status: "246 Frotzed",
820 StatusCode: 246,866 StatusCode: 246,
821 Body: ioutil.NopCloser(strings.NewReader("<Error><Code>Frotzed</Code><Message>failed to get blob</Message></Error>")),867 Body: makeResponseBody("<Error><Code>Frotzed</Code><Message>failed to get blob</Message></Error>"),
822 }868 }
823 transport := &TestTransport{Response: response}869 transport := &TestTransport{Response: response}
824 context := makeStorageContext()870 context := makeStorageContext()
825871
=== modified file 'testhelpers_http.go'
--- testhelpers_http.go 2013-04-24 10:08:54 +0000
+++ testhelpers_http.go 2013-04-25 14:03:27 +0000
@@ -7,35 +7,49 @@
77
8import (8import (
9 "fmt"9 "fmt"
10 "io"
11 "io/ioutil"
10 "net/http"12 "net/http"
13 "strings"
11)14)
1215
13// TestTransport is used as an http.Client.Transport for testing. The only16// TestTransport is used as an http.Client.Transport for testing. It records
14// requirement is that it adhere to the http.RoundTripper interface.17// the latest request, and returns a predetermined Response and error.
18// TODO: Why does this need to be exported?
15type TestTransport struct {19type TestTransport struct {
16 Request *http.Request20 Request *http.Request
17 Response *http.Response21 Response *http.Response
18 Error error22 Error error
19}23}
2024
25// TestTransport implements the http.RoundTripper interface.
26var _ http.RoundTripper = &TestTransport{}
27
21func (t *TestTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {28func (t *TestTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
22 t.Request = req29 t.Request = req
23 return t.Response, t.Error30 return t.Response, t.Error
24}31}
2532
33// TODO: Why does this need to be exported?
26type TestTransport2Exchange struct {34type TestTransport2Exchange struct {
27 Request *http.Request35 Request *http.Request
28 Response *http.Response36 Response *http.Response
29 Error error37 Error error
30}38}
3139
32// TestTransport2 is used as an http.Client.Transport for testing. The only40// TestTransport2 is used as an http.Client.Transport for testing. It records
33// requirement is that it adhere to the http.RoundTripper interface.41// the sequence of requests, and returns a predetermined sequence of Responses
42// and errors.
43// TODO: Rename this to something remotely sensible.
44// TODO: Why does this need to be exported?
34type TestTransport2 struct {45type TestTransport2 struct {
35 Exchanges []*TestTransport2Exchange46 Exchanges []*TestTransport2Exchange
36 ExchangeCount int47 ExchangeCount int
37}48}
3849
50// TestTransport2 implements the http.RoundTripper interface.
51var _ http.RoundTripper = &TestTransport2{}
52
39func (t *TestTransport2) AddExchange(response *http.Response, error error) {53func (t *TestTransport2) AddExchange(response *http.Response, error error) {
40 exchange := TestTransport2Exchange{Response: response, Error: error}54 exchange := TestTransport2Exchange{Response: response, Error: error}
41 t.Exchanges = append(t.Exchanges, &exchange)55 t.Exchanges = append(t.Exchanges, &exchange)
@@ -56,3 +70,10 @@
56 Body: Empty,70 Body: Empty,
57 }71 }
58}72}
73
74// makeResponseBody creates an http response body containing the given string.
75// Use this to initialize an http.Response.Body with a given string, without
76// having to care about the type details.
77func makeResponseBody(content string) io.ReadCloser {
78 return ioutil.NopCloser(strings.NewReader(content))
79}
5980
=== modified file 'testhelpers_misc.go'
--- testhelpers_misc.go 2013-04-24 10:08:54 +0000
+++ testhelpers_misc.go 2013-04-25 14:03:27 +0000
@@ -4,11 +4,9 @@
4package gwacl4package gwacl
55
6import (6import (
7 "bytes"
8 "encoding/base64"7 "encoding/base64"
9 "fmt"8 "fmt"
10 "io"9 "io"
11 "io/ioutil"
12)10)
1311
14// b64 is shorthand for base64-encoding a string.12// b64 is shorthand for base64-encoding a string.
@@ -17,7 +15,7 @@
17}15}
1816
19// A Reader and ReadCloser that EOFs immediately.17// A Reader and ReadCloser that EOFs immediately.
20var Empty io.ReadCloser = ioutil.NopCloser(bytes.NewReader(nil))18var Empty io.ReadCloser = makeResponseBody("")
2119
22// BoolToString represents a boolean value as a string ("true" or "false").20// BoolToString represents a boolean value as a string ("true" or "false").
23func BoolToString(v bool) string {21func BoolToString(v bool) string {

Subscribers

People subscribed via source and target branches