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
1=== modified file 'Makefile'
2--- Makefile 2013-04-24 08:36:17 +0000
3+++ Makefile 2013-04-25 14:03:27 +0000
4@@ -11,9 +11,7 @@
5
6 # Reformat the source files to match our layout standards.
7 # This includes gofmt's "simplify" option to streamline the source code.
8-# TODO: The -s run may actually also do the regular formatting.
9 format:
10- ./utilities/format
11 ./utilities/format -s
12
13 # Build the examples (we have no tests for them).
14
15=== modified file 'storage_base.go'
16--- storage_base.go 2013-04-22 16:00:31 +0000
17+++ storage_base.go 2013-04-25 14:03:27 +0000
18@@ -265,19 +265,61 @@
19 return resp, nil
20 }
21
22-// Send a request to the storage service to list the containers in the
23-// storage account. error is non-nil if an error occurred.
24+// getListContainersBatch calls the "List Containers" operation on the storage
25+// API, and returns a single batch of results; its "next marker" for batching,
26+// and an error code.
27+// The marker argument should be empty for a new List Containers request. for
28+// subsequent calls to get additional batches of the same result, pass the
29+// "next marker" returned by the previous call.
30+// The "next marker" will be empty on the last batch.
31+func (context *StorageContext) getListContainersBatch(marker string) (*ContainerEnumerationResults, string, error) {
32+ uri := interpolateURL(
33+ "http://___.blob.core.windows.net/?comp=list", context.Account)
34+ req, err := http.NewRequest("GET", uri, nil)
35+ if err != nil {
36+ return nil, "", err
37+ }
38+ addStandardHeaders(req, context.Account, context.Key, "2012-02-12")
39+ containers := ContainerEnumerationResults{}
40+ _, err = context.send(req, &containers, http.StatusOK)
41+ if err != nil {
42+ return nil, "", err
43+ }
44+
45+ // The response may contain a NextMarker field, to let us request a
46+ // subsequent batch of results. The XML parser won't trim whitespace out
47+ // of the marker tag, so we do that here.
48+ nextMarker := strings.TrimSpace(containers.NextMarker)
49+ return &containers, nextMarker, nil
50+}
51+
52+// ListContainers sends a request to the storage service to list the containers
53+// in the storage account. error is non-nil if an error occurred.
54 func (context *StorageContext) ListContainers() (*ContainerEnumerationResults, error) {
55- uri := interpolateURL(
56- "http://___.blob.core.windows.net/?comp=list", context.Account)
57- req, err := http.NewRequest("GET", uri, nil)
58- if err != nil {
59- return nil, err
60+ containers := make([]Container, 0)
61+ var batch *ContainerEnumerationResults
62+
63+ // Request the initial result, using the empty marker. Then, for as long
64+ // as the result has a nonempty NextMarker, request the next batch using
65+ // that marker.
66+ for marker, nextMarker := "", "x"; nextMarker != ""; marker = nextMarker {
67+ var err error
68+ // Don't use := here or you'll shadow variables from the function's
69+ // outer scopes.
70+ batch, nextMarker, err = context.getListContainersBatch(marker)
71+ if err != nil {
72+ return nil, err
73+ }
74+ containers = append(containers, batch.Containers...)
75 }
76- addStandardHeaders(req, context.Account, context.Key, "2012-02-12")
77- containers := &ContainerEnumerationResults{}
78- _, e := context.send(req, containers, http.StatusOK)
79- return containers, e
80+
81+ // There's more in a ContainerEnumerationResults than just the containers.
82+ // Return the latest batch, but give it the full cumulative containers list
83+ // instead of just the last batch.
84+ // To the caller, this will look like they made one call to Azure's
85+ // List Containers method, but batch size was unlimited.
86+ batch.Containers = containers
87+ return batch, nil
88 }
89
90 // Send a request to the storage service to list the blobs in a container.
91
92=== modified file 'storage_base_test.go'
93--- storage_base_test.go 2013-04-24 10:08:54 +0000
94+++ storage_base_test.go 2013-04-25 14:03:27 +0000
95@@ -295,13 +295,13 @@
96 </Metadata>
97 </Container>
98 </Containers>
99- <NextMarker>next-marker-value</NextMarker>
100+ <NextMarker/>
101 </EnumerationResults>`
102 context := makeStorageContext()
103 response := &http.Response{
104 Status: fmt.Sprintf("%d", http.StatusOK),
105 StatusCode: http.StatusOK,
106- Body: ioutil.NopCloser(strings.NewReader(response_body)),
107+ Body: makeResponseBody(response_body),
108 }
109 transport := &TestTransport{Response: response}
110 context.client = &http.Client{Transport: transport}
111@@ -337,6 +337,52 @@
112 c.Assert(err, NotNil)
113 }
114
115+// ListContainers combines multiple batches of output.
116+func (suite *TestListContainers) TestBatchedResult(c *C) {
117+ firstContainer := "container1"
118+ lastContainer := "container2"
119+ marker := "moreplease"
120+ firstBatch := http.Response{
121+ StatusCode: http.StatusOK,
122+ Body: makeResponseBody(fmt.Sprintf(`
123+ <EnumerationResults>
124+ <Containers>
125+ <Container>
126+ <Name>%s</Name>
127+ <URL>container-address</URL>
128+ </Container>
129+ </Containers>
130+ <NextMarker>%s</NextMarker>
131+ </EnumerationResults>
132+ `, firstContainer, marker)),
133+ }
134+ lastBatch := http.Response{
135+ StatusCode: http.StatusOK,
136+ Body: makeResponseBody(fmt.Sprintf(`
137+ <EnumerationResults>
138+ <Containers>
139+ <Container>
140+ <Name>%s</Name>
141+ <URL>container-address</URL>
142+ </Container>
143+ </Containers>
144+ </EnumerationResults>
145+ `, lastContainer)),
146+ }
147+ transport := TestTransport2{}
148+ transport.AddExchange(&firstBatch, nil)
149+ transport.AddExchange(&lastBatch, nil)
150+ context := makeStorageContext()
151+ context.client = &http.Client{Transport: &transport}
152+
153+ containers, err := context.ListContainers()
154+ c.Assert(err, IsNil)
155+
156+ c.Check(len(containers.Containers), Equals, 2)
157+ c.Check(containers.Containers[0].Name, Equals, firstContainer)
158+ c.Check(containers.Containers[1].Name, Equals, lastContainer)
159+}
160+
161 type TestListBlobs struct{}
162
163 var _ = Suite(&TestListBlobs{})
164@@ -392,7 +438,7 @@
165 response := &http.Response{
166 Status: fmt.Sprintf("%d", http.StatusOK),
167 StatusCode: http.StatusOK,
168- Body: ioutil.NopCloser(strings.NewReader(response_body)),
169+ Body: makeResponseBody(response_body),
170 }
171 transport := &TestTransport{Response: response}
172 context.client = &http.Client{Transport: transport}
173@@ -523,7 +569,7 @@
174 response := &http.Response{
175 Status: "102 Frotzed",
176 StatusCode: 102,
177- Body: ioutil.NopCloser(strings.NewReader("<Error><Code>Frotzed</Code><Message>failed to put blob</Message></Error>")),
178+ Body: makeResponseBody("<Error><Code>Frotzed</Code><Message>failed to put blob</Message></Error>"),
179 }
180 transport := &TestTransport{Response: response}
181 context := makeStorageContext()
182@@ -582,7 +628,7 @@
183 response := &http.Response{
184 Status: "102 Frotzed",
185 StatusCode: 102,
186- Body: ioutil.NopCloser(strings.NewReader("<Error><Code>Frotzed</Code><Message>failed to put block</Message></Error>")),
187+ Body: makeResponseBody("<Error><Code>Frotzed</Code><Message>failed to put block</Message></Error>"),
188 }
189 transport := &TestTransport{Response: response}
190 context := makeStorageContext()
191@@ -645,7 +691,7 @@
192 response := &http.Response{
193 Status: "102 Frotzed",
194 StatusCode: 102,
195- Body: ioutil.NopCloser(strings.NewReader("<Error><Code>Frotzed</Code><Message>failed to put blocklist</Message></Error>")),
196+ Body: makeResponseBody("<Error><Code>Frotzed</Code><Message>failed to put blocklist</Message></Error>"),
197 }
198 transport := &TestTransport{Response: response}
199 context := makeStorageContext()
200@@ -686,7 +732,7 @@
201 response := &http.Response{
202 Status: fmt.Sprintf("%d", http.StatusOK),
203 StatusCode: http.StatusOK,
204- Body: ioutil.NopCloser(strings.NewReader(response_body)),
205+ Body: makeResponseBody(response_body),
206 }
207 transport := &TestTransport{Response: response}
208 context.client = &http.Client{Transport: transport}
209@@ -760,7 +806,7 @@
210 response := &http.Response{
211 Status: "146 Frotzed",
212 StatusCode: 146,
213- Body: ioutil.NopCloser(strings.NewReader("<Error><Code>Frotzed</Code><Message>failed to delete blob</Message></Error>")),
214+ Body: makeResponseBody("<Error><Code>Frotzed</Code><Message>failed to delete blob</Message></Error>"),
215 }
216 transport := &TestTransport{Response: response}
217 context := makeStorageContext()
218@@ -782,7 +828,7 @@
219 response := &http.Response{
220 Status: fmt.Sprintf("%d", http.StatusOK),
221 StatusCode: http.StatusOK,
222- Body: ioutil.NopCloser(strings.NewReader(response_body)),
223+ Body: makeResponseBody(response_body),
224 }
225 transport := &TestTransport{Response: response}
226 context.client = &http.Client{Transport: transport}
227@@ -818,7 +864,7 @@
228 response := &http.Response{
229 Status: "246 Frotzed",
230 StatusCode: 246,
231- Body: ioutil.NopCloser(strings.NewReader("<Error><Code>Frotzed</Code><Message>failed to get blob</Message></Error>")),
232+ Body: makeResponseBody("<Error><Code>Frotzed</Code><Message>failed to get blob</Message></Error>"),
233 }
234 transport := &TestTransport{Response: response}
235 context := makeStorageContext()
236
237=== modified file 'testhelpers_http.go'
238--- testhelpers_http.go 2013-04-24 10:08:54 +0000
239+++ testhelpers_http.go 2013-04-25 14:03:27 +0000
240@@ -7,35 +7,49 @@
241
242 import (
243 "fmt"
244+ "io"
245+ "io/ioutil"
246 "net/http"
247+ "strings"
248 )
249
250-// TestTransport is used as an http.Client.Transport for testing. The only
251-// requirement is that it adhere to the http.RoundTripper interface.
252+// TestTransport is used as an http.Client.Transport for testing. It records
253+// the latest request, and returns a predetermined Response and error.
254+// TODO: Why does this need to be exported?
255 type TestTransport struct {
256 Request *http.Request
257 Response *http.Response
258 Error error
259 }
260
261+// TestTransport implements the http.RoundTripper interface.
262+var _ http.RoundTripper = &TestTransport{}
263+
264 func (t *TestTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
265 t.Request = req
266 return t.Response, t.Error
267 }
268
269+// TODO: Why does this need to be exported?
270 type TestTransport2Exchange struct {
271 Request *http.Request
272 Response *http.Response
273 Error error
274 }
275
276-// TestTransport2 is used as an http.Client.Transport for testing. The only
277-// requirement is that it adhere to the http.RoundTripper interface.
278+// TestTransport2 is used as an http.Client.Transport for testing. It records
279+// the sequence of requests, and returns a predetermined sequence of Responses
280+// and errors.
281+// TODO: Rename this to something remotely sensible.
282+// TODO: Why does this need to be exported?
283 type TestTransport2 struct {
284 Exchanges []*TestTransport2Exchange
285 ExchangeCount int
286 }
287
288+// TestTransport2 implements the http.RoundTripper interface.
289+var _ http.RoundTripper = &TestTransport2{}
290+
291 func (t *TestTransport2) AddExchange(response *http.Response, error error) {
292 exchange := TestTransport2Exchange{Response: response, Error: error}
293 t.Exchanges = append(t.Exchanges, &exchange)
294@@ -56,3 +70,10 @@
295 Body: Empty,
296 }
297 }
298+
299+// makeResponseBody creates an http response body containing the given string.
300+// Use this to initialize an http.Response.Body with a given string, without
301+// having to care about the type details.
302+func makeResponseBody(content string) io.ReadCloser {
303+ return ioutil.NopCloser(strings.NewReader(content))
304+}
305
306=== modified file 'testhelpers_misc.go'
307--- testhelpers_misc.go 2013-04-24 10:08:54 +0000
308+++ testhelpers_misc.go 2013-04-25 14:03:27 +0000
309@@ -4,11 +4,9 @@
310 package gwacl
311
312 import (
313- "bytes"
314 "encoding/base64"
315 "fmt"
316 "io"
317- "io/ioutil"
318 )
319
320 // b64 is shorthand for base64-encoding a string.
321@@ -17,7 +15,7 @@
322 }
323
324 // A Reader and ReadCloser that EOFs immediately.
325-var Empty io.ReadCloser = ioutil.NopCloser(bytes.NewReader(nil))
326+var Empty io.ReadCloser = makeResponseBody("")
327
328 // BoolToString represents a boolean value as a string ("true" or "false").
329 func BoolToString(v bool) string {

Subscribers

People subscribed via source and target branches