github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/store/blobstore/inmem.go (about) 1 // Copyright 2019 Dolthub, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package blobstore 16 17 import ( 18 "bytes" 19 "context" 20 "fmt" 21 "io" 22 "sync" 23 24 "golang.org/x/sync/errgroup" 25 26 "github.com/google/uuid" 27 ) 28 29 type byteSliceReadCloser struct { 30 io.Reader 31 io.Closer 32 } 33 34 func newByteSliceReadCloser(data []byte) *byteSliceReadCloser { 35 reader := bytes.NewReader(data) 36 return &byteSliceReadCloser{reader, io.NopCloser(reader)} 37 } 38 39 // InMemoryBlobstore provides an in memory implementation of the Blobstore interface 40 type InMemoryBlobstore struct { 41 path string 42 mutex sync.RWMutex 43 blobs map[string][]byte 44 versions map[string]string 45 } 46 47 var _ Blobstore = &InMemoryBlobstore{} 48 49 // NewInMemoryBlobstore creates an instance of an InMemoryBlobstore 50 func NewInMemoryBlobstore(path string) *InMemoryBlobstore { 51 return &InMemoryBlobstore{ 52 path: path, 53 blobs: make(map[string][]byte), 54 versions: make(map[string]string), 55 } 56 } 57 58 func (bs *InMemoryBlobstore) Path() string { 59 return bs.path 60 } 61 62 // Get retrieves an io.reader for the portion of a blob specified by br along with 63 // its version 64 func (bs *InMemoryBlobstore) Get(ctx context.Context, key string, br BlobRange) (io.ReadCloser, string, error) { 65 bs.mutex.Lock() 66 defer bs.mutex.Unlock() 67 68 if val, ok := bs.blobs[key]; ok { 69 if ver, ok := bs.versions[key]; ok && ver != "" { 70 var byteRange []byte 71 if br.isAllRange() { 72 byteRange = val 73 } else { 74 posBR := br.positiveRange(int64(len(val))) 75 if posBR.length == 0 { 76 byteRange = val[posBR.offset:] 77 } else { 78 byteRange = val[posBR.offset : posBR.offset+posBR.length] 79 } 80 } 81 82 return newByteSliceReadCloser(byteRange), ver, nil 83 } 84 85 panic("Blob without version, or with invalid version, should no be possible.") 86 } 87 88 return nil, "", NotFound{key} 89 } 90 91 // Put sets the blob and the version for a key 92 func (bs *InMemoryBlobstore) Put(ctx context.Context, key string, totalSize int64, reader io.Reader) (string, error) { 93 bs.mutex.Lock() 94 defer bs.mutex.Unlock() 95 return bs.put(ctx, key, reader) 96 } 97 98 // CheckAndPut will check the current version of a blob against an expectedVersion, and if the 99 // versions match it will update the data and version associated with the key 100 func (bs *InMemoryBlobstore) CheckAndPut(ctx context.Context, expectedVersion, key string, totalSize int64, reader io.Reader) (string, error) { 101 bs.mutex.Lock() 102 defer bs.mutex.Unlock() 103 104 ver, ok := bs.versions[key] 105 check := !ok && expectedVersion == "" || ok && expectedVersion == ver 106 107 if !check { 108 return "", CheckAndPutError{key, expectedVersion, ver} 109 } 110 return bs.put(ctx, key, reader) 111 } 112 113 // Exists returns true if a blob exists for the given key, and false if it does not. 114 // For InMemoryBlobstore instances error should never be returned (though other 115 // implementations of this interface can) 116 func (bs *InMemoryBlobstore) Exists(ctx context.Context, key string) (bool, error) { 117 _, ok := bs.blobs[key] 118 return ok, nil 119 } 120 121 func (bs *InMemoryBlobstore) Concatenate(ctx context.Context, key string, sources []string) (string, error) { 122 // recursively compose sources (mirrors GCS impl) 123 for len(sources) > composeBatch { 124 // compose subsets of |sources| in batches, 125 // store tmp composite objects in |next| 126 var next []string 127 var batches [][]string 128 for len(sources) > 0 { 129 k := min(composeBatch, len(sources)) 130 batches = append(batches, sources[:k]) 131 next = append(next, uuid.New().String()) 132 sources = sources[k:] 133 } 134 // execute compose calls concurrently (mirrors GCS impl) 135 eg, _ := errgroup.WithContext(ctx) 136 for i := 0; i < len(batches); i++ { 137 idx := i 138 eg.Go(func() error { 139 blob, err := bs.composeObjects(batches[idx]) 140 if err != nil { 141 return err 142 } 143 _, err = bs.Put(ctx, next[idx], int64(len(blob)), bytes.NewReader(blob)) 144 return err 145 }) 146 } 147 if err := eg.Wait(); err != nil { 148 return "", err 149 } 150 sources = next 151 } 152 153 blob, err := bs.composeObjects(sources) 154 if err != nil { 155 return "", err 156 } 157 return bs.Put(ctx, key, int64(len(blob)), bytes.NewReader(blob)) 158 } 159 160 func (bs *InMemoryBlobstore) put(ctx context.Context, key string, reader io.Reader) (string, error) { 161 ver := uuid.New().String() 162 data, err := io.ReadAll(reader) 163 164 if err != nil { 165 return "", err 166 } 167 168 bs.blobs[key] = data 169 bs.versions[key] = ver 170 171 return ver, nil 172 } 173 174 func (bs *InMemoryBlobstore) composeObjects(sources []string) (blob []byte, err error) { 175 bs.mutex.RLock() 176 defer bs.mutex.RUnlock() 177 if len(sources) > composeBatch { 178 return nil, fmt.Errorf("too many objects to compose (%d > %d)", len(sources), composeBatch) 179 } 180 var sz int 181 for _, k := range sources { 182 sz += len(bs.blobs[k]) 183 } 184 blob = make([]byte, 0, sz) 185 for _, k := range sources { 186 blob = append(blob, bs.blobs[k]...) 187 } 188 return 189 }