5d556b3e8c40eb242addf53f4996c49eb396138f
[arvados.git] / services / keepstore / azure_blob_volume_test.go
1 package main
2
3 import (
4         "bytes"
5         "crypto/md5"
6         "encoding/base64"
7         "encoding/xml"
8         "flag"
9         "fmt"
10         "io/ioutil"
11         "log"
12         "math/rand"
13         "net"
14         "net/http"
15         "net/http/httptest"
16         "regexp"
17         "sort"
18         "strconv"
19         "strings"
20         "sync"
21         "testing"
22         "time"
23
24         "github.com/curoverse/azure-sdk-for-go/storage"
25 )
26
27 const (
28         // The same fake credentials used by Microsoft's Azure emulator
29         emulatorAccountName = "devstoreaccount1"
30         emulatorAccountKey  = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
31 )
32
33 var azureTestContainer string
34
35 func init() {
36         flag.StringVar(
37                 &azureTestContainer,
38                 "test.azure-storage-container-volume",
39                 "",
40                 "Name of Azure container to use for testing. Do not use a container with real data! Use -azure-storage-account-name and -azure-storage-key-file arguments to supply credentials.")
41 }
42
43 type azBlob struct {
44         Data        []byte
45         Etag        string
46         Metadata    map[string]string
47         Mtime       time.Time
48         Uncommitted map[string][]byte
49 }
50
51 type azStubHandler struct {
52         sync.Mutex
53         blobs map[string]*azBlob
54         race  chan chan struct{}
55 }
56
57 func newAzStubHandler() *azStubHandler {
58         return &azStubHandler{
59                 blobs: make(map[string]*azBlob),
60         }
61 }
62
63 func (h *azStubHandler) TouchWithDate(container, hash string, t time.Time) {
64         blob, ok := h.blobs[container+"|"+hash]
65         if !ok {
66                 return
67         }
68         blob.Mtime = t
69 }
70
71 func (h *azStubHandler) PutRaw(container, hash string, data []byte) {
72         h.Lock()
73         defer h.Unlock()
74         h.blobs[container+"|"+hash] = &azBlob{
75                 Data:        data,
76                 Mtime:       time.Now(),
77                 Metadata:    make(map[string]string),
78                 Uncommitted: make(map[string][]byte),
79         }
80 }
81
82 func (h *azStubHandler) unlockAndRace() {
83         if h.race == nil {
84                 return
85         }
86         h.Unlock()
87         // Signal caller that race is starting by reading from
88         // h.race. If we get a channel, block until that channel is
89         // ready to receive. If we get nil (or h.race is closed) just
90         // proceed.
91         if c := <-h.race; c != nil {
92                 c <- struct{}{}
93         }
94         h.Lock()
95 }
96
97 var rangeRegexp = regexp.MustCompile(`^bytes=(\d+)-(\d+)$`)
98
99 func (h *azStubHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
100         h.Lock()
101         defer h.Unlock()
102         // defer log.Printf("azStubHandler: %+v", r)
103
104         path := strings.Split(r.URL.Path, "/")
105         container := path[1]
106         hash := ""
107         if len(path) > 2 {
108                 hash = path[2]
109         }
110
111         if err := r.ParseForm(); err != nil {
112                 log.Printf("azStubHandler(%+v): %s", r, err)
113                 rw.WriteHeader(http.StatusBadRequest)
114                 return
115         }
116
117         body, err := ioutil.ReadAll(r.Body)
118         if err != nil {
119                 return
120         }
121
122         type blockListRequestBody struct {
123                 XMLName     xml.Name `xml:"BlockList"`
124                 Uncommitted []string
125         }
126
127         blob, blobExists := h.blobs[container+"|"+hash]
128
129         switch {
130         case r.Method == "PUT" && r.Form.Get("comp") == "":
131                 // "Put Blob" API
132                 if _, ok := h.blobs[container+"|"+hash]; !ok {
133                         // Like the real Azure service, we offer a
134                         // race window during which other clients can
135                         // list/get the new blob before any data is
136                         // committed.
137                         h.blobs[container+"|"+hash] = &azBlob{
138                                 Mtime:       time.Now(),
139                                 Uncommitted: make(map[string][]byte),
140                                 Metadata:    make(map[string]string),
141                                 Etag:        makeEtag(),
142                         }
143                         h.unlockAndRace()
144                 }
145                 metadata := make(map[string]string)
146                 for k, v := range r.Header {
147                         if strings.HasPrefix(strings.ToLower(k), "x-ms-meta-") {
148                                 name := k[len("x-ms-meta-"):]
149                                 metadata[strings.ToLower(name)] = v[0]
150                         }
151                 }
152                 h.blobs[container+"|"+hash] = &azBlob{
153                         Data:        body,
154                         Mtime:       time.Now(),
155                         Uncommitted: make(map[string][]byte),
156                         Metadata:    metadata,
157                         Etag:        makeEtag(),
158                 }
159                 rw.WriteHeader(http.StatusCreated)
160         case r.Method == "PUT" && r.Form.Get("comp") == "block":
161                 // "Put Block" API
162                 if !blobExists {
163                         log.Printf("Got block for nonexistent blob: %+v", r)
164                         rw.WriteHeader(http.StatusBadRequest)
165                         return
166                 }
167                 blockID, err := base64.StdEncoding.DecodeString(r.Form.Get("blockid"))
168                 if err != nil || len(blockID) == 0 {
169                         log.Printf("Invalid blockid: %+q", r.Form.Get("blockid"))
170                         rw.WriteHeader(http.StatusBadRequest)
171                         return
172                 }
173                 blob.Uncommitted[string(blockID)] = body
174                 rw.WriteHeader(http.StatusCreated)
175         case r.Method == "PUT" && r.Form.Get("comp") == "blocklist":
176                 // "Put Block List" API
177                 bl := &blockListRequestBody{}
178                 if err := xml.Unmarshal(body, bl); err != nil {
179                         log.Printf("xml Unmarshal: %s", err)
180                         rw.WriteHeader(http.StatusBadRequest)
181                         return
182                 }
183                 for _, encBlockID := range bl.Uncommitted {
184                         blockID, err := base64.StdEncoding.DecodeString(encBlockID)
185                         if err != nil || len(blockID) == 0 || blob.Uncommitted[string(blockID)] == nil {
186                                 log.Printf("Invalid blockid: %+q", encBlockID)
187                                 rw.WriteHeader(http.StatusBadRequest)
188                                 return
189                         }
190                         blob.Data = blob.Uncommitted[string(blockID)]
191                         blob.Etag = makeEtag()
192                         blob.Mtime = time.Now()
193                         delete(blob.Uncommitted, string(blockID))
194                 }
195                 rw.WriteHeader(http.StatusCreated)
196         case r.Method == "PUT" && r.Form.Get("comp") == "metadata":
197                 // "Set Metadata Headers" API. We don't bother
198                 // stubbing "Get Metadata Headers": AzureBlobVolume
199                 // sets metadata headers only as a way to bump Etag
200                 // and Last-Modified.
201                 if !blobExists {
202                         log.Printf("Got metadata for nonexistent blob: %+v", r)
203                         rw.WriteHeader(http.StatusBadRequest)
204                         return
205                 }
206                 blob.Metadata = make(map[string]string)
207                 for k, v := range r.Header {
208                         if strings.HasPrefix(strings.ToLower(k), "x-ms-meta-") {
209                                 name := k[len("x-ms-meta-"):]
210                                 blob.Metadata[strings.ToLower(name)] = v[0]
211                         }
212                 }
213                 blob.Mtime = time.Now()
214                 blob.Etag = makeEtag()
215         case (r.Method == "GET" || r.Method == "HEAD") && r.Form.Get("comp") == "metadata" && hash != "":
216                 // "Get Blob Metadata" API
217                 if !blobExists {
218                         rw.WriteHeader(http.StatusNotFound)
219                         return
220                 }
221                 for k, v := range blob.Metadata {
222                         rw.Header().Set(fmt.Sprintf("x-ms-meta-%s", k), v)
223                 }
224                 return
225         case (r.Method == "GET" || r.Method == "HEAD") && hash != "":
226                 // "Get Blob" API
227                 if !blobExists {
228                         rw.WriteHeader(http.StatusNotFound)
229                         return
230                 }
231                 data := blob.Data
232                 if rangeSpec := rangeRegexp.FindStringSubmatch(r.Header.Get("Range")); rangeSpec != nil {
233                         b0, err0 := strconv.Atoi(rangeSpec[1])
234                         b1, err1 := strconv.Atoi(rangeSpec[2])
235                         if err0 != nil || err1 != nil || b0 >= len(data) || b1 >= len(data) || b0 > b1 {
236                                 rw.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", len(data)))
237                                 rw.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
238                                 return
239                         }
240                         rw.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", b0, b1, len(data)))
241                         rw.WriteHeader(http.StatusPartialContent)
242                         data = data[b0 : b1+1]
243                 }
244                 rw.Header().Set("Last-Modified", blob.Mtime.Format(time.RFC1123))
245                 rw.Header().Set("Content-Length", strconv.Itoa(len(data)))
246                 if r.Method == "GET" {
247                         if _, err := rw.Write(data); err != nil {
248                                 log.Printf("write %+q: %s", data, err)
249                         }
250                 }
251                 h.unlockAndRace()
252         case r.Method == "DELETE" && hash != "":
253                 // "Delete Blob" API
254                 if !blobExists {
255                         rw.WriteHeader(http.StatusNotFound)
256                         return
257                 }
258                 delete(h.blobs, container+"|"+hash)
259                 rw.WriteHeader(http.StatusAccepted)
260         case r.Method == "GET" && r.Form.Get("comp") == "list" && r.Form.Get("restype") == "container":
261                 // "List Blobs" API
262                 prefix := container + "|" + r.Form.Get("prefix")
263                 marker := r.Form.Get("marker")
264
265                 maxResults := 2
266                 if n, err := strconv.Atoi(r.Form.Get("maxresults")); err == nil && n >= 1 && n <= 5000 {
267                         maxResults = n
268                 }
269
270                 resp := storage.BlobListResponse{
271                         Marker:     marker,
272                         NextMarker: "",
273                         MaxResults: int64(maxResults),
274                 }
275                 var hashes sort.StringSlice
276                 for k := range h.blobs {
277                         if strings.HasPrefix(k, prefix) {
278                                 hashes = append(hashes, k[len(container)+1:])
279                         }
280                 }
281                 hashes.Sort()
282                 for _, hash := range hashes {
283                         if len(resp.Blobs) == maxResults {
284                                 resp.NextMarker = hash
285                                 break
286                         }
287                         if len(resp.Blobs) > 0 || marker == "" || marker == hash {
288                                 blob := h.blobs[container+"|"+hash]
289                                 bmeta := map[string]string(nil)
290                                 if r.Form.Get("include") == "metadata" {
291                                         bmeta = blob.Metadata
292                                 }
293                                 b := storage.Blob{
294                                         Name: hash,
295                                         Properties: storage.BlobProperties{
296                                                 LastModified:  blob.Mtime.Format(time.RFC1123),
297                                                 ContentLength: int64(len(blob.Data)),
298                                                 Etag:          blob.Etag,
299                                         },
300                                         Metadata: bmeta,
301                                 }
302                                 resp.Blobs = append(resp.Blobs, b)
303                         }
304                 }
305                 buf, err := xml.Marshal(resp)
306                 if err != nil {
307                         log.Print(err)
308                         rw.WriteHeader(http.StatusInternalServerError)
309                 }
310                 rw.Write(buf)
311         default:
312                 log.Printf("azStubHandler: not implemented: %+v Body:%+q", r, body)
313                 rw.WriteHeader(http.StatusNotImplemented)
314         }
315 }
316
317 // azStubDialer is a net.Dialer that notices when the Azure driver
318 // tries to connect to "devstoreaccount1.blob.127.0.0.1:46067", and
319 // in such cases transparently dials "127.0.0.1:46067" instead.
320 type azStubDialer struct {
321         net.Dialer
322 }
323
324 var localHostPortRe = regexp.MustCompile(`(127\.0\.0\.1|localhost|\[::1\]):\d+`)
325
326 func (d *azStubDialer) Dial(network, address string) (net.Conn, error) {
327         if hp := localHostPortRe.FindString(address); hp != "" {
328                 log.Println("azStubDialer: dial", hp, "instead of", address)
329                 address = hp
330         }
331         return d.Dialer.Dial(network, address)
332 }
333
334 type TestableAzureBlobVolume struct {
335         *AzureBlobVolume
336         azHandler *azStubHandler
337         azStub    *httptest.Server
338         t         TB
339 }
340
341 func NewTestableAzureBlobVolume(t TB, readonly bool, replication int) *TestableAzureBlobVolume {
342         azHandler := newAzStubHandler()
343         azStub := httptest.NewServer(azHandler)
344
345         var azClient storage.Client
346
347         container := azureTestContainer
348         if container == "" {
349                 // Connect to stub instead of real Azure storage service
350                 stubURLBase := strings.Split(azStub.URL, "://")[1]
351                 var err error
352                 if azClient, err = storage.NewClient(emulatorAccountName, emulatorAccountKey, stubURLBase, storage.DefaultAPIVersion, false); err != nil {
353                         t.Fatal(err)
354                 }
355                 container = "fakecontainername"
356         } else {
357                 // Connect to real Azure storage service
358                 accountKey, err := readKeyFromFile(azureStorageAccountKeyFile)
359                 if err != nil {
360                         t.Fatal(err)
361                 }
362                 azClient, err = storage.NewBasicClient(azureStorageAccountName, accountKey)
363                 if err != nil {
364                         t.Fatal(err)
365                 }
366         }
367
368         v := NewAzureBlobVolume(azClient, container, readonly, replication)
369
370         return &TestableAzureBlobVolume{
371                 AzureBlobVolume: v,
372                 azHandler:       azHandler,
373                 azStub:          azStub,
374                 t:               t,
375         }
376 }
377
378 func TestAzureBlobVolumeWithGeneric(t *testing.T) {
379         defer func(t http.RoundTripper) {
380                 http.DefaultTransport = t
381         }(http.DefaultTransport)
382         http.DefaultTransport = &http.Transport{
383                 Dial: (&azStubDialer{}).Dial,
384         }
385         azureWriteRaceInterval = time.Millisecond
386         azureWriteRacePollTime = time.Nanosecond
387         DoGenericVolumeTests(t, func(t TB) TestableVolume {
388                 return NewTestableAzureBlobVolume(t, false, azureStorageReplication)
389         })
390 }
391
392 func TestAzureBlobVolumeConcurrentRanges(t *testing.T) {
393         defer func(b int) {
394                 azureMaxGetBytes = b
395         }(azureMaxGetBytes)
396
397         defer func(t http.RoundTripper) {
398                 http.DefaultTransport = t
399         }(http.DefaultTransport)
400         http.DefaultTransport = &http.Transport{
401                 Dial: (&azStubDialer{}).Dial,
402         }
403         azureWriteRaceInterval = time.Millisecond
404         azureWriteRacePollTime = time.Nanosecond
405         // Test (BlockSize mod azureMaxGetBytes)==0 and !=0 cases
406         for _, azureMaxGetBytes = range []int{2 << 22, 2<<22 - 1} {
407                 DoGenericVolumeTests(t, func(t TB) TestableVolume {
408                         return NewTestableAzureBlobVolume(t, false, azureStorageReplication)
409                 })
410         }
411 }
412
413 func TestReadonlyAzureBlobVolumeWithGeneric(t *testing.T) {
414         defer func(t http.RoundTripper) {
415                 http.DefaultTransport = t
416         }(http.DefaultTransport)
417         http.DefaultTransport = &http.Transport{
418                 Dial: (&azStubDialer{}).Dial,
419         }
420         azureWriteRaceInterval = time.Millisecond
421         azureWriteRacePollTime = time.Nanosecond
422         DoGenericVolumeTests(t, func(t TB) TestableVolume {
423                 return NewTestableAzureBlobVolume(t, true, azureStorageReplication)
424         })
425 }
426
427 func TestAzureBlobVolumeRangeFenceposts(t *testing.T) {
428         defer func(t http.RoundTripper) {
429                 http.DefaultTransport = t
430         }(http.DefaultTransport)
431         http.DefaultTransport = &http.Transport{
432                 Dial: (&azStubDialer{}).Dial,
433         }
434
435         v := NewTestableAzureBlobVolume(t, false, 3)
436         defer v.Teardown()
437
438         for _, size := range []int{
439                 2<<22 - 1, // one <max read
440                 2 << 22,   // one =max read
441                 2<<22 + 1, // one =max read, one <max
442                 2 << 23,   // two =max reads
443                 BlockSize - 1,
444                 BlockSize,
445         } {
446                 data := make([]byte, size)
447                 for i := range data {
448                         data[i] = byte((i + 7) & 0xff)
449                 }
450                 hash := fmt.Sprintf("%x", md5.Sum(data))
451                 err := v.Put(hash, data)
452                 if err != nil {
453                         t.Error(err)
454                 }
455                 gotData := make([]byte, len(data))
456                 gotLen, err := v.Get(hash, gotData)
457                 if err != nil {
458                         t.Error(err)
459                 }
460                 gotHash := fmt.Sprintf("%x", md5.Sum(gotData))
461                 if gotLen != size {
462                         t.Error("length mismatch: got %d != %d", gotLen, size)
463                 }
464                 if gotHash != hash {
465                         t.Error("hash mismatch: got %s != %s", gotHash, hash)
466                 }
467         }
468 }
469
470 func TestAzureBlobVolumeReplication(t *testing.T) {
471         for r := 1; r <= 4; r++ {
472                 v := NewTestableAzureBlobVolume(t, false, r)
473                 defer v.Teardown()
474                 if n := v.Replication(); n != r {
475                         t.Errorf("Got replication %d, expected %d", n, r)
476                 }
477         }
478 }
479
480 func TestAzureBlobVolumeCreateBlobRace(t *testing.T) {
481         defer func(t http.RoundTripper) {
482                 http.DefaultTransport = t
483         }(http.DefaultTransport)
484         http.DefaultTransport = &http.Transport{
485                 Dial: (&azStubDialer{}).Dial,
486         }
487
488         v := NewTestableAzureBlobVolume(t, false, 3)
489         defer v.Teardown()
490
491         azureWriteRaceInterval = time.Second
492         azureWriteRacePollTime = time.Millisecond
493
494         allDone := make(chan struct{})
495         v.azHandler.race = make(chan chan struct{})
496         go func() {
497                 err := v.Put(TestHash, TestBlock)
498                 if err != nil {
499                         t.Error(err)
500                 }
501         }()
502         continuePut := make(chan struct{})
503         // Wait for the stub's Put to create the empty blob
504         v.azHandler.race <- continuePut
505         go func() {
506                 buf := make([]byte, len(TestBlock))
507                 _, err := v.Get(TestHash, buf)
508                 if err != nil {
509                         t.Error(err)
510                 }
511                 close(allDone)
512         }()
513         // Wait for the stub's Get to get the empty blob
514         close(v.azHandler.race)
515         // Allow stub's Put to continue, so the real data is ready
516         // when the volume's Get retries
517         <-continuePut
518         // Wait for volume's Get to return the real data
519         <-allDone
520 }
521
522 func TestAzureBlobVolumeCreateBlobRaceDeadline(t *testing.T) {
523         defer func(t http.RoundTripper) {
524                 http.DefaultTransport = t
525         }(http.DefaultTransport)
526         http.DefaultTransport = &http.Transport{
527                 Dial: (&azStubDialer{}).Dial,
528         }
529
530         v := NewTestableAzureBlobVolume(t, false, 3)
531         defer v.Teardown()
532
533         azureWriteRaceInterval = 2 * time.Second
534         azureWriteRacePollTime = 5 * time.Millisecond
535
536         v.PutRaw(TestHash, nil)
537
538         buf := new(bytes.Buffer)
539         v.IndexTo("", buf)
540         if buf.Len() != 0 {
541                 t.Errorf("Index %+q should be empty", buf.Bytes())
542         }
543
544         v.TouchWithDate(TestHash, time.Now().Add(-1982*time.Millisecond))
545
546         allDone := make(chan struct{})
547         go func() {
548                 defer close(allDone)
549                 buf := make([]byte, BlockSize)
550                 n, err := v.Get(TestHash, buf)
551                 if err != nil {
552                         t.Error(err)
553                         return
554                 }
555                 if n != 0 {
556                         t.Errorf("Got %+q, expected empty buf", buf[:n])
557                 }
558         }()
559         select {
560         case <-allDone:
561         case <-time.After(time.Second):
562                 t.Error("Get should have stopped waiting for race when block was 2s old")
563         }
564
565         buf.Reset()
566         v.IndexTo("", buf)
567         if !bytes.HasPrefix(buf.Bytes(), []byte(TestHash+"+0")) {
568                 t.Errorf("Index %+q should have %+q", buf.Bytes(), TestHash+"+0")
569         }
570 }
571
572 func (v *TestableAzureBlobVolume) PutRaw(locator string, data []byte) {
573         v.azHandler.PutRaw(v.containerName, locator, data)
574 }
575
576 func (v *TestableAzureBlobVolume) TouchWithDate(locator string, lastPut time.Time) {
577         v.azHandler.TouchWithDate(v.containerName, locator, lastPut)
578 }
579
580 func (v *TestableAzureBlobVolume) Teardown() {
581         v.azStub.Close()
582 }
583
584 func makeEtag() string {
585         return fmt.Sprintf("0x%x", rand.Int63())
586 }