Merge lp:~allenap/gwacl/chunky-blobs-operation into lp:gwacl

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: 118
Merged at revision: 93
Proposed branch: lp:~allenap/gwacl/chunky-blobs-operation
Merge into: lp:gwacl
Diff against target: 449 lines (+318/-41)
7 files modified
example/storage/run.go (+4/-32)
logging/logging.go (+83/-0)
logging/logging_test.go (+39/-0)
storage.go (+47/-0)
storage_test.go (+130/-0)
test_helpers.go (+6/-0)
x509dispatcher.go (+9/-9)
To merge this branch: bzr merge lp:~allenap/gwacl/chunky-blobs-operation
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+159512@code.launchpad.net

Commit message

New Storage API meta-operation UploadBlockBlob that uses PutBlock and PutBlockList to upload larger files in chunks.

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :

61 === added file 'storage2.go'

Can you not think of a better name for this? In fact I'm not even sure we need another file. If you insist, we could have a new file called storage_blob.go and move all the blob stuff into it.

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

[sorry. hit the button too early]

91 + if err == io.EOF {
92 + break
93 + }
94 + if err != nil {
95 + return err
96 + }

I looked at this and thought, why not a switch. But then the break would break the wrong thing... gah :/

98 + blockID := strconv.FormatInt(blockNum, 36)

The documentation says that all block IDs should be the same length, and suggests base64 encoding this ID. Additionally the docs say "the pre-encoded string must be 64 bytes or less". It might be worth enforcing/checking that here.

99 + log.Printf("Uploading block %d (size=%d, id=%s).\n",
109 + log.Printf("Committing %d blocks.\n", len(blockList.Items))

I don't think you should be logging here :)

You don't have to do it here but one of the advantages of block uploads is that we can do them in parallel. As a side project you could add go routines to do that here, up to a configured maximum in parallel. It'll speed up the uploads considerably.

167 + Body: ioutil.NopCloser(bytes.NewReader(nil)),

We've used this NopCloser thing in a few places now, do you think we should turn it into a test helper?

160 +func (suite *testUploadBlockBlob) TestNoHeaders(c *C) {

Did you copy this and forget to change the function name?

172 + Status: fmt.Sprintf("%d", http.StatusOK),
173 + StatusCode: http.StatusCreated,

Discrepancy between statuses here, is that intentional? If so, please comment why.

I don't know if it's how Go just is but I found it quite hard to discern the test intentions in this large test. I think we should break it up for readability, and create some re-usable test helpers at the same time.

If we had something along the lines of this, it would more clearly show test intent:
[in pseudo code]
    insertFakeResponse(status=..., body=)
    makeRandomData()
    uploadBlockBlob()
    assertPutBlockSent(url, data)
    assertBlockListSent(url, data)

This way we've abstracted out the boilerplate, but the test data is all contained here in the same function so it's easy to see input and expected output, and the test intention.

review: Needs Fixing
100. By Gavin Panella

Create Empty, an io.ReadCloser counterpart to ioutil.Discard.

101. By Gavin Panella

Fix copy-n-paste error.

102. By Gavin Panella

Fix typo.

Revision history for this message
Gavin Panella (allenap) wrote :
Download full text (3.5 KiB)

> [sorry. hit the button too early]
>
> 91      +        if err == io.EOF {
> 92      +            break
> 93      +        }
> 94      +        if err != nil {
> 95      +            return err
> 96      +        }
>
> I looked at this and thought, why not a switch.  But then the break would
> break the wrong thing... gah :/

Yeah, that was the same problem I encountered. I could use a label,
but they're just as ugly.

>
> 98      +        blockID := strconv.FormatInt(blockNum, 36)
>
> The documentation says that all block IDs should be the same length, and
> suggests base64 encoding this ID. Additionally the docs say "the pre-encoded
> string must be 64 bytes or less".  It might be worth enforcing/checking that
> here.

By my calculations we won't have a problem here until blockNum is a
little over 4e99. There's belt and braces, then there's girdle,
suspenders and nappies. I think it'll be fine :)

>
> 99      +        log.Printf("Uploading block %d (size=%d, id=%s).\n",
> 109     +    log.Printf("Committing %d blocks.\n", len(blockList.Items))
>
> I don't think you should be logging here :)

I think I should be logging here, but for Go's pretend log package,
which is not much more than a wrapper around print right now.
Crucially there's no way to suppress these messages. If you object
strongly I'll remove them, but they are useful when uploading bigger
files.

>
> You don't have to do it here but one of the advantages of block uploads is
> that we can do them in parallel.  As a side project you could add go routines
> to do that here, up to a configured maximum in parallel.  It'll speed up the
> uploads considerably.

Will it? If network utilisation is near 100% then doing more
concurrently won't speed it up. I guess it would mean that more TCP
slow-starts could happen together, instead of each time there's a
connection. Beyond that I'm not sure. I want to get my teeth into some
Go-style concurrency, but I'm not sure this warrants it.

>
> 167     +        Body:       ioutil.NopCloser(bytes.NewReader(nil)),
>
> We've used this NopCloser thing in a few places now, do you think we should
> turn it into a test helper?

Done: I've created Empty in test_helpers.go, which is a io.ReadCloser
counterpart to ioutil.Discard.

>
> 160     +func (suite *testUploadBlockBlob) TestNoHeaders(c *C) {
>
> Did you copy this and forget to change the function name?

Oops, yes.

>
> 172     +        Status:     fmt.Sprintf("%d", http.StatusOK),
> 173     +        StatusCode: http.StatusCreated,
>
> Discrepancy between statuses here, is that intentional?  If so, please comment
> why.

It was a mistake. Fixed.

>
> I don't know if it's how Go just is but I found it quite hard to discern the
> test intentions in this large test.  I think we should break it up for
> readability, and create some re-usable test helpers at the same time.
>
> If we had something along the lines of this, it would more clearly show test
> intent:
> [in pseudo code]
>     insertFakeResponse(status=..., body=)
>     makeRandomData()
>     uploadBlockBlob()
>     assertPutBlockSent(url, data)
>     assertBlockListSent(url, data)
>
> This way we've abstracted out the boilerplate, but the tes...

Read more...

Revision history for this message
Gavin Panella (allenap) wrote :

> 61      === added file 'storage2.go'
>
> Can you not think of a better name for this?  In fact I'm not even sure we
> need another file.  If you insist, we could have a new file called
> storage_blob.go and move all the blob stuff into it.

I tried first to refactor the storage stuff into a subpackage, but
that turned into a nightmare, and I backed out. The other file was
getting a bit long, and I wanted to put these higher-level operations
somewhere distinct from the lower-level API building blocks. As such,
I don't think storage_blob.go is the right name. Perhaps renaming
storage.go to storage_base.go, and storage2.go to storage.go?

103. By Gavin Panella

Extract method makeFakeCreatedResponse().

104. By Gavin Panella

Extract methods uploadRandomBlob(), assertBlockSent(), and assertBlockListSent().

105. By Gavin Panella

Move makeFakeCreatedResponse() down.

106. By Gavin Panella

Additional test for a large file that actually needs chunking.

Revision history for this message
Gavin Panella (allenap) wrote :

I think this is ready for another look now. In addition to responding to your review comments, I've also added a second test for a larger file, one that needs to be chunked.

Revision history for this message
Julian Edwards (julian-edwards) wrote :
Download full text (3.6 KiB)

On 20/04/13 00:50, Gavin Panella wrote:
> Yeah, that was the same problem I encountered. I could use a label,
> but they're just as ugly.

I reckon this is why Go still has a goto ...

>>
>> 98 + blockID := strconv.FormatInt(blockNum, 36)
>>
>> The documentation says that all block IDs should be the same length, and
>> suggests base64 encoding this ID. Additionally the docs say "the pre-encoded
>> string must be 64 bytes or less". It might be worth enforcing/checking that
>> here.
>
> By my calculations we won't have a problem here until blockNum is a
> little over 4e99. There's belt and braces, then there's girdle,
> suspenders and nappies. I think it'll be fine :)

I guess I am getting ahead of myself a bit, I was thinking of something
I know will happen at some point where people will want to supply their
own block IDs.

Does FormatInt guarantee the same length string for all numbers? That
does seem to be a hard requirement of the API.

>>
>> 99 + log.Printf("Uploading block %d (size=%d, id=%s).\n",
>> 109 + log.Printf("Committing %d blocks.\n", len(blockList.Items))
>>
>> I don't think you should be logging here :)
>
> I think I should be logging here, but for Go's pretend log package,
> which is not much more than a wrapper around print right now.
> Crucially there's no way to suppress these messages. If you object
> strongly I'll remove them, but they are useful when uploading bigger
> files.

I am strongly of the opinion that a library function should not
unconditionally log. At least change to a Debugf.

>> You don't have to do it here but one of the advantages of block uploads is
>> that we can do them in parallel. As a side project you could add go routines
>> to do that here, up to a configured maximum in parallel. It'll speed up the
>> uploads considerably.
>
> Will it?

Yes.

> If network utilisation is near 100% then doing more

You're not necessarily guaranteed 100% net utilisation from a single TCP
connection. Especially over international links. I can get much better
speeds here if I have more than one session at once, because handshaking
has such a high latency that the TCP window still fails to do anything
useful.

> concurrently won't speed it up. I guess it would mean that more TCP
> slow-starts could happen together, instead of each time there's a
> connection. Beyond that I'm not sure. I want to get my teeth into some
> Go-style concurrency, but I'm not sure this warrants it.

Trust me, it does warrant it. I do a lot of downloading from Australia
and I see real speed gains with multiple sessions.

Like I said, we don't need it right now in this branch but I think it
should be done. Besides, you want to play with goroutines, right? :)

>
>>
>> 167 + Body: ioutil.NopCloser(bytes.NewReader(nil)),
>>
>> We've used this NopCloser thing in a few places now, do you think we should
>> turn it into a test helper?
>
> Done: I've created Empty in test_helpers.go, which is a io.ReadCloser
> counterpart to ioutil.Discard.

Coolio, thanks! (I can't help but pronounce Body in the manner of Fat
Bastard from Austin Powers)

> I like this idea. Fwiw, I did pass this by The Mi...

Read more...

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

On 20/04/13 00:55, Gavin Panella wrote:
>> 61 === added file 'storage2.go'
>>
>> Can you not think of a better name for this? In fact I'm not even sure we
>> need another file. If you insist, we could have a new file called
>> storage_blob.go and move all the blob stuff into it.
>
> I tried first to refactor the storage stuff into a subpackage, but
> that turned into a nightmare, and I backed out. The other file was
> getting a bit long, and I wanted to put these higher-level operations
> somewhere distinct from the lower-level API building blocks. As such,
> I don't think storage_blob.go is the right name. Perhaps renaming
> storage.go to storage_base.go, and storage2.go to storage.go?
>

On reflection I think it's early optimisation (if there is such a thing
for file organisation). Do you strongly object to just leaving it all
in the same file?

Otherwise your suggestion is fine.

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

+ // block ID is a base64 encoding of the string "0".
+ assertBlockSent(c, context, data, "MA==", transport.Exchanges[0])

You could use:
base64.StdEncoding.EncodeToString([]byte("0"))
like we do in the rest of the gwacl tests.

Everything else looks great, thanks.

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

> Does FormatInt guarantee the same length string for all numbers?  That
> does seem to be a hard requirement of the API.

Ah, no, it doesn't. I haven't seen that requirement before... /me
investigates.

...
> I am strongly of the opinion that a library function should not
> unconditionally log.  At least change to a Debugf.

Yeah, I agree with that. It's a rubbish logging package. I'll do
something to make sure it's quiet.

>
> >> You don't have to do it here but one of the advantages of block uploads is
> >> that we can do them in parallel.  As a side project you could add go
> routines
> >> to do that here, up to a configured maximum in parallel.  It'll speed up
> the
> >> uploads considerably.
> >
> > Will it?
>
> Yes.
>
> > If network utilisation is near 100% then doing more
>
> You're not necessarily guaranteed 100% net utilisation from a single TCP
> connection.  Especially over international links.  I can get much better
> speeds here if I have more than one session at once, because handshaking
> has such a high latency that the TCP window still fails to do anything
> useful.
>
> > concurrently won't speed it up. I guess it would mean that more TCP
> > slow-starts could happen together, instead of each time there's a
> > connection. Beyond that I'm not sure. I want to get my teeth into some
> > Go-style concurrency, but I'm not sure this warrants it.
>
> Trust me, it does warrant it.  I do a lot of downloading from Australia
> and I see real speed gains with multiple sessions.
>
> Like I said, we don't need it right now in this branch but I think it
> should be done.  Besides, you want to play with goroutines, right? :)

Yes. It's about all the pleasure there's to be had from Go. Okay, I'll
try to add it when I'm stalling on something else.

Revision history for this message
Gavin Panella (allenap) wrote :

> On reflection I think it's early optimisation (if there is such a thing
> for file organisation). Do you strongly object to just leaving it all
> in the same file?

I do, if only because it's more work to split it out than it is to
merge it later if we decide we want it that way. It all appears in the
gwacl package, but I'd like to maintain a mental fence between the API
atoms and the things that are built with them.

107. By Gavin Panella

Add logging package to wrap the standard library's log package into something just about useful.

108. By Gavin Panella

Use the new logging stuff in storage2.go.

109. By Gavin Panella

Remove superflous semicolon.

110. By Gavin Panella

Encode the blockIDs in the test, instead of hard-coding the base64 versions.

111. By Gavin Panella

Add GPL header to logging.go.

112. By Gavin Panella

Make WARN the default log level.

113. By Gavin Panella

Set log.level from the environment.

114. By Gavin Panella

Renames.

115. By Gavin Panella

No need to define level's type.

116. By Gavin Panella

Add a rudimentary test for the logging stuff.

117. By Gavin Panella

Move logging stuff into subpackage.

118. By Gavin Panella

Use the logging package in x509dispatcher too.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'example/storage/run.go'
2--- example/storage/run.go 2013-04-17 10:11:07 +0000
3+++ example/storage/run.go 2013-04-22 16:24:27 +0000
4@@ -10,15 +10,11 @@
5 package main
6
7 import (
8- "bytes"
9 "flag"
10 "fmt"
11- "io"
12 "io/ioutil"
13 "launchpad.net/gwacl"
14- "log"
15 "os"
16- "strconv"
17 )
18
19 var account string
20@@ -125,35 +121,11 @@
21 }
22 defer file.Close()
23
24- buffer := make([]byte, 1024*1024) // 1MB buffer
25- blockList := &gwacl.BlockList{}
26-
27- // Upload the file in chunks.
28- for blockNum := int64(0); ; blockNum++ {
29- blockSize, err := file.Read(buffer)
30- if err == io.EOF {
31- break
32- }
33- if err != nil {
34- dumpError(err)
35- return
36- }
37- block := bytes.NewReader(buffer[:blockSize])
38- blockID := strconv.FormatInt(blockNum, 36)
39- log.Printf("Uploading block %d (size=%d, id=%s).\n",
40- blockNum, blockSize, blockID)
41- err = storage.PutBlock(container, filename, blockID, block)
42- if err != nil {
43- dumpError(err)
44- return
45- }
46- blockList.Add(gwacl.BlockListLatest, blockID)
47+ err = storage.UploadBlockBlob(container, filename, file)
48+ if err != nil {
49+ dumpError(err)
50+ return
51 }
52-
53- // Commit those blocks by writing the block list.
54- log.Printf("Committing %d blocks.\n", len(blockList.Items))
55- err = storage.PutBlockList(container, filename, blockList)
56- dumpError(err)
57 }
58
59 func deleteblob(storage gwacl.StorageContext) {
60
61=== added directory 'logging'
62=== added file 'logging/logging.go'
63--- logging/logging.go 1970-01-01 00:00:00 +0000
64+++ logging/logging.go 2013-04-22 16:24:27 +0000
65@@ -0,0 +1,83 @@
66+// Copyright 2013 Canonical Ltd. This software is licensed under the
67+// GNU Lesser General Public License version 3 (see the file COPYING).
68+
69+package logging
70+
71+import (
72+ "log"
73+ "os"
74+)
75+
76+const (
77+ DEBUG = 10 * (iota + 1)
78+ INFO
79+ WARN
80+ ERROR
81+)
82+
83+var level = WARN
84+
85+func init() {
86+ setLevel(os.Getenv("LOGLEVEL"))
87+}
88+
89+func setLevel(levelName string) {
90+ switch levelName {
91+ case "DEBUG":
92+ level = DEBUG
93+ case "INFO":
94+ level = INFO
95+ case "WARN":
96+ level = WARN
97+ case "ERROR":
98+ level = ERROR
99+ }
100+}
101+
102+func Debug(args ...interface{}) {
103+ if level <= DEBUG {
104+ log.Println(args...)
105+ }
106+}
107+
108+func Debugf(format string, args ...interface{}) {
109+ if level <= DEBUG {
110+ log.Printf(format, args...)
111+ }
112+}
113+
114+func Info(args ...interface{}) {
115+ if level <= INFO {
116+ log.Println(args...)
117+ }
118+}
119+
120+func Infof(format string, args ...interface{}) {
121+ if level <= INFO {
122+ log.Printf(format, args...)
123+ }
124+}
125+
126+func Warn(args ...interface{}) {
127+ if level <= WARN {
128+ log.Println(args...)
129+ }
130+}
131+
132+func Warnf(format string, args ...interface{}) {
133+ if level <= WARN {
134+ log.Printf(format, args...)
135+ }
136+}
137+
138+func Error(args ...interface{}) {
139+ if level <= ERROR {
140+ log.Println(args...)
141+ }
142+}
143+
144+func Errorf(format string, args ...interface{}) {
145+ if level <= ERROR {
146+ log.Printf(format, args...)
147+ }
148+}
149
150=== added file 'logging/logging_test.go'
151--- logging/logging_test.go 1970-01-01 00:00:00 +0000
152+++ logging/logging_test.go 2013-04-22 16:24:27 +0000
153@@ -0,0 +1,39 @@
154+// Copyright 2013 Canonical Ltd. This software is licensed under the
155+// GNU Lesser General Public License version 3 (see the file COPYING).
156+
157+package logging
158+
159+import (
160+ . "launchpad.net/gocheck"
161+)
162+
163+var originalLevel = level
164+
165+func restoreLevel() {
166+ level = originalLevel
167+}
168+
169+type testLogging struct{}
170+
171+var _ = Suite(&testLogging{})
172+
173+func (suite *testLogging) TestSetLevel(c *C) {
174+ defer restoreLevel()
175+ // The names of the logging constants are recognised arguments to
176+ // setLevel().
177+ level = -1
178+ setLevel("DEBUG")
179+ c.Check(level, Equals, DEBUG)
180+ setLevel("INFO")
181+ c.Check(level, Equals, INFO)
182+ setLevel("WARN")
183+ c.Check(level, Equals, WARN)
184+ setLevel("ERROR")
185+ c.Check(level, Equals, ERROR)
186+ // Unrecognised arguments are ignored.
187+ level = -1
188+ setLevel("FOOBAR")
189+ c.Check(level, Equals, -1)
190+ setLevel("123")
191+ c.Check(level, Equals, -1)
192+}
193
194=== added file 'storage.go'
195--- storage.go 1970-01-01 00:00:00 +0000
196+++ storage.go 2013-04-22 16:24:27 +0000
197@@ -0,0 +1,47 @@
198+// Copyright 2013 Canonical Ltd. This software is licensed under the
199+// GNU Lesser General Public License version 3 (see the file COPYING).
200+
201+package gwacl
202+
203+// This file contains higher level operations necessary to work with the Azure
204+// file storage API.
205+
206+import (
207+ "bytes"
208+ "io"
209+ . "launchpad.net/gwacl/logging"
210+ "strconv"
211+)
212+
213+// UploadBlockBlob uses PutBlock and PutBlockList API operations to upload
214+// arbitrarily large files, 1MB at a time.
215+func (context *StorageContext) UploadBlockBlob(
216+ container, filename string, data io.Reader) error {
217+
218+ buffer := make([]byte, 1024*1024) // 1MB buffer
219+ blockList := &BlockList{}
220+
221+ // Upload the file in chunks.
222+ for blockNum := int64(0); ; blockNum++ {
223+ blockSize, err := data.Read(buffer)
224+ if err == io.EOF {
225+ break
226+ }
227+ if err != nil {
228+ return err
229+ }
230+ block := bytes.NewReader(buffer[:blockSize])
231+ blockID := strconv.FormatInt(blockNum, 36)
232+ Debugf("Uploading block %d (size=%d, id=%s).\n",
233+ blockNum, blockSize, blockID)
234+ err = context.PutBlock(container, filename, blockID, block)
235+ if err != nil {
236+ return err
237+ }
238+ blockList.Add(BlockListLatest, blockID)
239+ }
240+
241+ // Commit those blocks by writing the block list.
242+ Debugf("Committing %d blocks.\n", len(blockList.Items))
243+ return context.PutBlockList(container, filename, blockList)
244+}
245
246=== renamed file 'storage.go' => 'storage_base.go'
247=== renamed file 'storage_test.go' => 'storage_base_test.go'
248=== added file 'storage_test.go'
249--- storage_test.go 1970-01-01 00:00:00 +0000
250+++ storage_test.go 2013-04-22 16:24:27 +0000
251@@ -0,0 +1,130 @@
252+// Copyright 2013 Canonical Ltd. This software is licensed under the
253+// GNU Lesser General Public License version 3 (see the file COPYING).
254+
255+package gwacl
256+
257+import (
258+ "bytes"
259+ "encoding/base64"
260+ "fmt"
261+ "io/ioutil"
262+ . "launchpad.net/gocheck"
263+ "net/http"
264+ "net/url"
265+)
266+
267+func b64(s string) string {
268+ return base64.StdEncoding.EncodeToString([]byte(s))
269+}
270+
271+type TestTransport2Exchange struct {
272+ Request *http.Request
273+ Response *http.Response
274+ Error error
275+}
276+
277+// TestTransport2 is used as an http.Client.Transport for testing. The only
278+// requirement is that it adhere to the http.RoundTripper interface.
279+type TestTransport2 struct {
280+ Exchanges []*TestTransport2Exchange
281+ ExchangeCount int
282+}
283+
284+func (t *TestTransport2) AddExchange(response *http.Response, error error) {
285+ exchange := TestTransport2Exchange{Response: response, Error: error}
286+ t.Exchanges = append(t.Exchanges, &exchange)
287+}
288+
289+func (t *TestTransport2) RoundTrip(req *http.Request) (resp *http.Response, err error) {
290+ exchange := t.Exchanges[t.ExchangeCount]
291+ t.ExchangeCount += 1
292+ exchange.Request = req
293+ return exchange.Response, exchange.Error
294+}
295+
296+type testUploadBlockBlob struct{}
297+
298+var _ = Suite(&testUploadBlockBlob{})
299+
300+func (suite *testUploadBlockBlob) TestSmallFile(c *C) {
301+ context := makeStorageContext()
302+ transport := &TestTransport2{}
303+ // UploadBlockBlob uses PutBlock to upload the data.
304+ transport.AddExchange(makeFakeCreatedResponse(), nil)
305+ // UploadBlockBlob then sends the list of blocks with PutBlockList.
306+ transport.AddExchange(makeFakeCreatedResponse(), nil)
307+ // Plug in the test transport.
308+ context.client = &http.Client{Transport: transport}
309+ // Upload a random blob of data.
310+ data := uploadRandomBlob(c, context, 10)
311+ // There were two exchanges.
312+ c.Assert(transport.ExchangeCount, Equals, 2)
313+ // The first request is a Put Block with the block data.
314+ assertBlockSent(c, context, data, b64("0"), transport.Exchanges[0])
315+ // The second request is Put Block List to commit the block above.
316+ assertBlockListSent(c, context, []string{b64("0")}, transport.Exchanges[1])
317+}
318+
319+func (suite *testUploadBlockBlob) TestLargeFile(c *C) {
320+ context := makeStorageContext()
321+ transport := &TestTransport2{}
322+ // UploadBlockBlob uses PutBlock twice to upload the data.
323+ transport.AddExchange(makeFakeCreatedResponse(), nil)
324+ transport.AddExchange(makeFakeCreatedResponse(), nil)
325+ // UploadBlockBlob then sends the list of blocks with PutBlockList.
326+ transport.AddExchange(makeFakeCreatedResponse(), nil)
327+ // Plug in the test transport.
328+ context.client = &http.Client{Transport: transport}
329+ // Upload a large random blob of data.
330+ data := uploadRandomBlob(c, context, 1348*1024)
331+ // There were three exchanges.
332+ c.Assert(transport.ExchangeCount, Equals, 3)
333+ // The first two requests are Put Block with chunks of the block data. The
334+ // weird looking block IDs are base64 encodings of the strings "0" and "1".
335+ assertBlockSent(c, context, data[:1024*1024], b64("0"), transport.Exchanges[0])
336+ assertBlockSent(c, context, data[1024*1024:], b64("1"), transport.Exchanges[1])
337+ // The second request is Put Block List to commit the block above.
338+ assertBlockListSent(c, context, []string{b64("0"), b64("1")}, transport.Exchanges[2])
339+}
340+
341+func makeFakeCreatedResponse() *http.Response {
342+ return &http.Response{
343+ Status: fmt.Sprintf("%d", http.StatusCreated),
344+ StatusCode: http.StatusCreated,
345+ Body: Empty,
346+ }
347+}
348+
349+func uploadRandomBlob(c *C, context *StorageContext, size int) []byte {
350+ data := MakeRandomByteSlice(size)
351+ err := context.UploadBlockBlob(
352+ "MyContainer", "MyFile", bytes.NewReader(data))
353+ c.Assert(err, IsNil)
354+ return data
355+}
356+
357+func assertBlockSent(
358+ c *C, context *StorageContext, data []byte, blockID string, exchange *TestTransport2Exchange) {
359+ c.Check(exchange.Request.URL.String(), Equals, fmt.Sprintf(
360+ "http://%s.blob.core.windows.net/MyContainer/MyFile"+
361+ "?comp=block&blockid=%s",
362+ context.Account, url.QueryEscape(blockID)))
363+ body, err := ioutil.ReadAll(exchange.Request.Body)
364+ c.Check(err, IsNil)
365+ c.Check(body, DeepEquals, data)
366+}
367+
368+func assertBlockListSent(
369+ c *C, context *StorageContext, blockIDs []string, exchange *TestTransport2Exchange) {
370+ c.Check(exchange.Request.URL.String(), Equals, fmt.Sprintf(
371+ "http://%s.blob.core.windows.net/MyContainer/MyFile"+
372+ "?comp=blocklist", context.Account))
373+ body, err := ioutil.ReadAll(exchange.Request.Body)
374+ c.Check(err, IsNil)
375+ expected := "\n<BlockList>\n"
376+ for _, blockID := range blockIDs {
377+ expected += " <Latest>" + blockID + "</Latest>\n"
378+ }
379+ expected += "</BlockList>"
380+ c.Check(string(body), Equals, expected)
381+}
382
383=== modified file 'test_helpers.go'
384--- test_helpers.go 2013-04-02 08:51:18 +0000
385+++ test_helpers.go 2013-04-22 16:24:27 +0000
386@@ -4,7 +4,10 @@
387 package gwacl
388
389 import (
390+ "bytes"
391 "fmt"
392+ "io"
393+ "io/ioutil"
394 "math/rand"
395 "net/http"
396 "strings"
397@@ -15,6 +18,9 @@
398 // Perhaps we can add it to gocheck, or start a testtools-like project.
399 const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz"
400
401+// A Reader and ReadCloser that EOFs immediately.
402+var Empty io.ReadCloser = ioutil.NopCloser(bytes.NewReader(nil))
403+
404 func MakeRandomString(length int) string {
405 return string(MakeRandomByteSlice(length))
406 }
407
408=== modified file 'x509dispatcher.go'
409--- x509dispatcher.go 2013-03-26 06:50:50 +0000
410+++ x509dispatcher.go 2013-04-22 16:24:27 +0000
411@@ -5,7 +5,7 @@
412 "bytes"
413 "fmt"
414 curl "github.com/andelf/go-curl"
415- "log"
416+ . "launchpad.net/gwacl/logging"
417 "net/http"
418 "net/textproto"
419 )
420@@ -143,10 +143,10 @@
421
422 func performX509CurlRequest(session *x509Session, request *x509Request) (*x509Response, error) {
423 if verbose {
424- log.Println("Performing request")
425- log.Println("Request url: " + request.URL)
426- log.Println("Request method: " + request.Method)
427- log.Printf("Request body: %s\n", request.Payload)
428+ Debug("Performing request")
429+ Debug("Request url: " + request.URL)
430+ Debug("Request method: " + request.Method)
431+ Debugf("Request body: %s\n", request.Payload)
432 }
433 response := newX509Response()
434 ch := request.makeCurlRequest(session, response)
435@@ -170,10 +170,10 @@
436 }
437 response.StatusCode = status.(int)
438 if verbose {
439- log.Println("Got response")
440- log.Printf("Response status: %d\n", response.StatusCode)
441- log.Printf("Response headers: %s\n", response.RawHeader)
442- log.Printf("Response body: %s\n", response.Body)
443+ Debug("Got response")
444+ Debugf("Response status: %d\n", response.StatusCode)
445+ Debugf("Response headers: %s\n", response.RawHeader)
446+ Debugf("Response body: %s\n", response.Body)
447 }
448 return response, nil
449 }

Subscribers

People subscribed via source and target branches

to all changes: