github.com/m3db/m3@v1.5.0/src/dbnode/persist/fs/index_lookup_test.go (about) 1 // Copyright (c) 2017 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 package fs 21 22 import ( 23 "bytes" 24 "fmt" 25 "io/ioutil" 26 "os" 27 "sort" 28 "strconv" 29 "sync" 30 "testing" 31 32 "github.com/m3db/m3/src/dbnode/digest" 33 "github.com/m3db/m3/src/dbnode/persist/fs/msgpack" 34 "github.com/m3db/m3/src/dbnode/persist/schema" 35 "github.com/m3db/m3/src/x/ident" 36 "github.com/m3db/m3/src/x/mmap" 37 38 "github.com/stretchr/testify/require" 39 ) 40 41 func TestNewNearestIndexOffsetDetectsUnsortedFiles(t *testing.T) { 42 // Create a slice of out-of-order index summary entries 43 outOfOrderSummaries := []schema.IndexSummary{ 44 { 45 Index: 0, 46 ID: []byte("1"), 47 IndexEntryOffset: 0, 48 }, 49 { 50 Index: 1, 51 ID: []byte("0"), 52 IndexEntryOffset: 10, 53 }, 54 } 55 56 // Create a temp file 57 file, err := ioutil.TempFile("", "index-lookup-sort") 58 require.NoError(t, err) 59 defer os.Remove(file.Name()) 60 61 // Write out the out-of-order summaries into the temp file 62 writeSummariesEntries(t, file, outOfOrderSummaries) 63 64 // Prepare the digest reader 65 summariesFdWithDigest := digest.NewFdWithDigestReader(4096) 66 file.Seek(0, 0) 67 summariesFdWithDigest.Reset(file) 68 69 // Determine the expected digest 70 expectedDigest := calculateExpectedDigest(t, summariesFdWithDigest) 71 72 // Reset the digest reader 73 file.Seek(0, 0) 74 summariesFdWithDigest.Reset(file) 75 76 // Try and create the index lookup and make sure it detects the file is out 77 // of order 78 _, err = newNearestIndexOffsetLookupFromSummariesFile( 79 summariesFdWithDigest, 80 expectedDigest, 81 msgpack.NewDecoder(nil), 82 msgpack.NewByteDecoderStream(nil), 83 len(outOfOrderSummaries), 84 false, 85 mmap.ReporterOptions{}, 86 ) 87 expectedErr := fmt.Errorf("summaries file is not sorted: %s", file.Name()) 88 require.Equal(t, expectedErr, err) 89 } 90 91 func TestCloneCannotBeCloned(t *testing.T) { 92 indexLookup := newNearestIndexOffsetLookup(nil, mmap.Descriptor{}) 93 clone, err := indexLookup.concurrentClone() 94 require.NoError(t, err) 95 96 _, err = clone.concurrentClone() 97 require.Error(t, err) 98 require.NoError(t, indexLookup.close()) 99 require.NoError(t, clone.close()) 100 } 101 102 func TestClosingCloneDoesNotAffectParent(t *testing.T) { 103 indexSummaries := []schema.IndexSummary{ 104 { 105 Index: 0, 106 ID: []byte("0"), 107 IndexEntryOffset: 0, 108 }, 109 { 110 Index: 1, 111 ID: []byte("1"), 112 IndexEntryOffset: 10, 113 }, 114 } 115 116 indexLookup := newIndexLookupWithSummaries(t, indexSummaries, false) 117 clone, err := indexLookup.concurrentClone() 118 require.NoError(t, err) 119 require.NoError(t, clone.close()) 120 for _, summary := range indexSummaries { 121 id := ident.StringID(string(summary.ID)) 122 require.NoError(t, err) 123 offset, err := clone.getNearestIndexFileOffset(id, newTestReusableSeekerResources()) 124 require.NoError(t, err) 125 require.Equal(t, summary.IndexEntryOffset, offset) 126 id.Finalize() 127 } 128 require.NoError(t, indexLookup.close()) 129 } 130 131 func TestParentAndClonesSafeForConcurrentUse(t *testing.T) { 132 testParentAndClonesSafeForConcurrentUse(t, false) 133 } 134 135 func TestParentAndClonesSafeForConcurrentUseForceMmapMemory(t *testing.T) { 136 testParentAndClonesSafeForConcurrentUse(t, true) 137 } 138 139 func testParentAndClonesSafeForConcurrentUse(t *testing.T, forceMmapMemory bool) { 140 numSummaries := 1000 141 numClones := 10 142 143 // Create test summary entries 144 indexSummaries := []schema.IndexSummary{} 145 for i := 0; i < numSummaries; i++ { 146 indexSummaries = append(indexSummaries, schema.IndexSummary{ 147 Index: int64(i), 148 ID: []byte(strconv.Itoa(i)), 149 IndexEntryOffset: int64(10 * i), 150 }) 151 } 152 sort.Sort(sortableSummaries(indexSummaries)) 153 154 // Create indexLookup and associated clones 155 indexLookup := newIndexLookupWithSummaries(t, indexSummaries, forceMmapMemory) 156 clones := []*nearestIndexOffsetLookup{} 157 for i := 0; i < numClones; i++ { 158 clone, err := indexLookup.concurrentClone() 159 require.NoError(t, err) 160 clones = append(clones, clone) 161 } 162 163 // Spin up a goroutine for each clone that looks up every offset. Use one waitgroup 164 // to make a best effort attempt to get all the goroutines active before they start 165 // doing work, and then another waitgroup to wait for them to all finish their work. 166 startWg := sync.WaitGroup{} 167 doneWg := sync.WaitGroup{} 168 startWg.Add(len(clones) + 1) 169 doneWg.Add(len(clones) + 1) 170 171 lookupOffsetsFunc := func(clone *nearestIndexOffsetLookup) { 172 startWg.Done() 173 startWg.Wait() 174 for _, summary := range indexSummaries { 175 id := ident.StringID(string(summary.ID)) 176 offset, err := clone.getNearestIndexFileOffset(id, newTestReusableSeekerResources()) 177 require.NoError(t, err) 178 require.Equal(t, summary.IndexEntryOffset, offset) 179 id.Finalize() 180 } 181 doneWg.Done() 182 } 183 go lookupOffsetsFunc(indexLookup) 184 for _, clone := range clones { 185 go lookupOffsetsFunc(clone) 186 } 187 188 // Wait for all workers to finish and then make sure everything can be cleaned 189 // up properly 190 doneWg.Wait() 191 require.NoError(t, indexLookup.close()) 192 for _, clone := range clones { 193 require.NoError(t, clone.close()) 194 } 195 } 196 197 // newIndexLookupWithSummaries will return a new index lookup that is backed by the provided 198 // indexSummaries (in the order that they are provided). 199 func newIndexLookupWithSummaries( 200 t *testing.T, indexSummaries []schema.IndexSummary, forceMmapMemory bool) *nearestIndexOffsetLookup { 201 // Create a temp file 202 file, err := ioutil.TempFile("", "index-lookup-sort") 203 require.NoError(t, err) 204 defer os.Remove(file.Name()) 205 206 writeSummariesEntries(t, file, indexSummaries) 207 208 // Prepare the digest reader 209 summariesFdWithDigest := digest.NewFdWithDigestReader(4096) 210 file.Seek(0, 0) 211 summariesFdWithDigest.Reset(file) 212 213 // Determine the expected digest 214 expectedDigest := calculateExpectedDigest(t, summariesFdWithDigest) 215 216 // Reset the digest reader 217 file.Seek(0, 0) 218 summariesFdWithDigest.Reset(file) 219 220 // Try and create the index lookup and make sure it detects the file is out 221 // of order 222 indexLookup, err := newNearestIndexOffsetLookupFromSummariesFile( 223 summariesFdWithDigest, 224 expectedDigest, 225 msgpack.NewDecoder(nil), 226 msgpack.NewByteDecoderStream(nil), 227 len(indexSummaries), 228 forceMmapMemory, 229 mmap.ReporterOptions{}, 230 ) 231 require.NoError(t, err) 232 return indexLookup 233 } 234 235 func writeSummariesEntries(t *testing.T, fd *os.File, summaries []schema.IndexSummary) { 236 encoder := msgpack.NewEncoder() 237 for _, summary := range summaries { 238 encoder.Reset() 239 require.NoError(t, encoder.EncodeIndexSummary(summary)) 240 _, err := fd.Write(encoder.Bytes()) 241 require.NoError(t, err) 242 } 243 } 244 245 func calculateExpectedDigest(t *testing.T, digestReader digest.FdWithDigestReader) uint32 { 246 // Determine the size of the file 247 file := digestReader.Fd() 248 stat, err := file.Stat() 249 require.NoError(t, err) 250 fileSize := stat.Size() 251 252 // Calculate the digest 253 _, err = digestReader.Read(make([]byte, fileSize)) 254 require.NoError(t, err) 255 return digestReader.Digest().Sum32() 256 } 257 258 type sortableSummaries []schema.IndexSummary 259 260 func (s sortableSummaries) Len() int { 261 return len(s) 262 } 263 264 func (s sortableSummaries) Less(i, j int) bool { 265 return bytes.Compare(s[i].ID, s[j].ID) < 0 266 } 267 268 func (s sortableSummaries) Swap(i, j int) { 269 s[i], s[j] = s[j], s[i] 270 }