github.com/m3db/m3@v1.5.0/src/dbnode/integration/index_helpers.go (about) 1 // +build integration 2 // 3 // Copyright (c) 2016 Uber Technologies, Inc. 4 // 5 // Permission is hereby granted, free of charge, to any person obtaining a copy 6 // of this software and associated documentation files (the "Software"), to deal 7 // in the Software without restriction, including without limitation the rights 8 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 // copies of the Software, and to permit persons to whom the Software is 10 // furnished to do so, subject to the following conditions: 11 // 12 // The above copyright notice and this permission notice shall be included in 13 // all copies or substantial portions of the Software. 14 // 15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 // THE SOFTWARE. 22 23 package integration 24 25 import ( 26 "context" 27 "fmt" 28 "strconv" 29 "testing" 30 "time" 31 32 "github.com/stretchr/testify/require" 33 "go.uber.org/zap" 34 35 "github.com/m3db/m3/src/dbnode/client" 36 "github.com/m3db/m3/src/dbnode/encoding" 37 "github.com/m3db/m3/src/dbnode/storage/index" 38 "github.com/m3db/m3/src/m3ninx/idx" 39 "github.com/m3db/m3/src/query/storage/m3/consolidators" 40 "github.com/m3db/m3/src/x/ident" 41 xtime "github.com/m3db/m3/src/x/time" 42 ) 43 44 // TestIndexWrites holds index writes for testing. 45 type TestIndexWrites []TestIndexWrite 46 47 // TestSeriesIterator is a minimal subset of encoding.SeriesIterator. 48 type TestSeriesIterator interface { 49 encoding.Iterator 50 51 // ID gets the ID of the series. 52 ID() ident.ID 53 54 // Tags returns an iterator over the tags associated with the ID. 55 Tags() ident.TagIterator 56 } 57 58 // TestSeriesIterators is a an iterator over TestSeriesIterator. 59 type TestSeriesIterators interface { 60 61 // Next moves to the next item. 62 Next() bool 63 64 // Current returns the current value. 65 Current() TestSeriesIterator 66 } 67 68 type testSeriesIterators struct { 69 encoding.SeriesIterators 70 idx int 71 } 72 73 func (t *testSeriesIterators) Next() bool { 74 if t.idx >= t.Len() { 75 return false 76 } 77 t.idx++ 78 79 return true 80 } 81 82 func (t *testSeriesIterators) Current() TestSeriesIterator { 83 return t.Iters()[t.idx-1] 84 } 85 86 // MatchesSeriesIters matches index writes with expected series. 87 func (w TestIndexWrites) MatchesSeriesIters( 88 t *testing.T, 89 seriesIters encoding.SeriesIterators, 90 ) { 91 actualCount := w.MatchesTestSeriesIters(t, &testSeriesIterators{SeriesIterators: seriesIters}) 92 93 uniqueIDs := make(map[string]struct{}) 94 for _, wi := range w { 95 uniqueIDs[wi.ID.String()] = struct{}{} 96 } 97 require.Equal(t, len(uniqueIDs), actualCount) 98 } 99 100 // MatchesTestSeriesIters matches index writes with expected test series. 101 func (w TestIndexWrites) MatchesTestSeriesIters( 102 t *testing.T, 103 seriesIters TestSeriesIterators, 104 ) int { 105 writesByID := make(map[string]TestIndexWrites) 106 for _, wi := range w { 107 writesByID[wi.ID.String()] = append(writesByID[wi.ID.String()], wi) 108 } 109 var actualCount int 110 for seriesIters.Next() { 111 iter := seriesIters.Current() 112 id := iter.ID().String() 113 writes, ok := writesByID[id] 114 require.True(t, ok, id) 115 writes.matchesSeriesIter(t, iter) 116 actualCount++ 117 } 118 119 return actualCount 120 } 121 122 func (w TestIndexWrites) matchesSeriesIter(t *testing.T, iter TestSeriesIterator) { 123 found := make([]bool, len(w)) 124 count := 0 125 for iter.Next() { 126 count++ 127 dp, _, _ := iter.Current() 128 for i := 0; i < len(w); i++ { 129 if found[i] { 130 continue 131 } 132 wi := w[i] 133 if !ident.NewTagIterMatcher(wi.Tags.Duplicate()).Matches(iter.Tags().Duplicate()) { 134 require.FailNow(t, "tags don't match provided id", iter.ID().String()) 135 } 136 if dp.TimestampNanos.Equal(wi.Timestamp) && dp.Value == wi.Value { 137 found[i] = true 138 break 139 } 140 } 141 } 142 require.Equal(t, len(w), count, iter.ID().String()) 143 require.NoError(t, iter.Err()) 144 for i := 0; i < len(found); i++ { 145 require.True(t, found[i], iter.ID().String()) 146 } 147 } 148 149 // Write writes test data and asserts the result. 150 func (w TestIndexWrites) Write(t *testing.T, ns ident.ID, s client.Session) { 151 require.NoError(t, w.WriteAttempt(ns, s)) 152 } 153 154 // WriteAttempt writes test data and returns an error if encountered. 155 func (w TestIndexWrites) WriteAttempt(ns ident.ID, s client.Session) error { 156 for i := 0; i < len(w); i++ { 157 wi := w[i] 158 err := s.WriteTagged(ns, wi.ID, wi.Tags.Duplicate(), wi.Timestamp, 159 wi.Value, xtime.Second, nil) 160 if err != nil { 161 return err 162 } 163 } 164 return nil 165 } 166 167 // NumIndexed gets number of indexed series. 168 func (w TestIndexWrites) NumIndexed(t *testing.T, ns ident.ID, s client.Session) int { 169 return w.NumIndexedWithOptions(t, ns, s, NumIndexedOptions{}) 170 } 171 172 // NumIndexedOptions is options when performing num indexed check. 173 type NumIndexedOptions struct { 174 Logger *zap.Logger 175 } 176 177 // NumIndexedWithOptions gets number of indexed series with a set of options. 178 func (w TestIndexWrites) NumIndexedWithOptions( 179 t *testing.T, 180 ns ident.ID, 181 s client.Session, 182 opts NumIndexedOptions, 183 ) int { 184 numFound := 0 185 for i := 0; i < len(w); i++ { 186 wi := w[i] 187 q := newQuery(t, wi.Tags) 188 iter, _, err := s.FetchTaggedIDs(ContextWithDefaultTimeout(), ns, 189 index.Query{Query: q}, 190 index.QueryOptions{ 191 StartInclusive: wi.Timestamp.Add(-1 * time.Second), 192 EndExclusive: wi.Timestamp.Add(1 * time.Second), 193 SeriesLimit: 10, 194 }) 195 if err != nil { 196 if l := opts.Logger; l != nil { 197 l.Error("fetch tagged IDs error", zap.Error(err)) 198 } 199 continue 200 } 201 if !iter.Next() { 202 if l := opts.Logger; l != nil { 203 l.Warn("missing result", 204 zap.String("queryID", wi.ID.String()), 205 zap.ByteString("queryTags", consolidators.MustIdentTagIteratorToTags(wi.Tags, nil).ID())) 206 } 207 continue 208 } 209 cuNs, cuID, cuTag := iter.Current() 210 if ns.String() != cuNs.String() { 211 if l := opts.Logger; l != nil { 212 l.Warn("namespace mismatch", 213 zap.String("queryNamespace", ns.String()), 214 zap.String("resultNamespace", cuNs.String())) 215 } 216 continue 217 } 218 if wi.ID.String() != cuID.String() { 219 if l := opts.Logger; l != nil { 220 l.Warn("id mismatch", 221 zap.String("queryID", wi.ID.String()), 222 zap.String("resultID", cuID.String())) 223 } 224 continue 225 } 226 if !ident.NewTagIterMatcher(wi.Tags).Matches(cuTag) { 227 if l := opts.Logger; l != nil { 228 l.Warn("tag mismatch", 229 zap.ByteString("queryTags", consolidators.MustIdentTagIteratorToTags(wi.Tags, nil).ID()), 230 zap.ByteString("resultTags", consolidators.MustIdentTagIteratorToTags(cuTag, nil).ID())) 231 } 232 continue 233 } 234 numFound++ 235 } 236 return numFound 237 } 238 239 type TestIndexWrite struct { 240 ID ident.ID 241 Tags ident.TagIterator 242 Timestamp xtime.UnixNano 243 Value float64 244 } 245 246 // GenerateTestIndexWrite generates test index writes. 247 func GenerateTestIndexWrite(periodID, numWrites, numTags int, startTime, endTime xtime.UnixNano) TestIndexWrites { 248 writes := make([]TestIndexWrite, 0, numWrites) 249 step := endTime.Sub(startTime) / time.Duration(numWrites+1) 250 for i := 0; i < numWrites; i++ { 251 id, tags := genIDTags(periodID, i, numTags) 252 writes = append(writes, TestIndexWrite{ 253 ID: id, 254 Tags: tags, 255 Timestamp: startTime.Add(time.Duration(i) * step).Truncate(time.Second), 256 Value: float64(i), 257 }) 258 } 259 return writes 260 } 261 262 type genIDTagsOption func(ident.Tags) ident.Tags 263 264 func genIDTags(i int, j int, numTags int, opts ...genIDTagsOption) (ident.ID, ident.TagIterator) { 265 id := fmt.Sprintf("foo.%d.%d", i, j) 266 tags := make([]ident.Tag, 0, numTags) 267 for i := 0; i < numTags; i++ { 268 tags = append(tags, ident.StringTag( 269 fmt.Sprintf("%s.tagname.%d", id, i), 270 fmt.Sprintf("%s.tagvalue.%d", id, i), 271 )) 272 } 273 tags = append(tags, 274 ident.StringTag("common_i", strconv.Itoa(i)), 275 ident.StringTag("common_j", strconv.Itoa(j)), 276 ident.StringTag("shared", "shared")) 277 278 result := ident.NewTags(tags...) 279 for _, fn := range opts { 280 result = fn(result) 281 } 282 283 return ident.StringID(id), ident.NewTagsIterator(result) 284 } 285 286 func isIndexed(t *testing.T, s client.Session, ns ident.ID, id ident.ID, tags ident.TagIterator) bool { 287 result, err := isIndexedChecked(t, s, ns, id, tags) 288 if err != nil { 289 return false 290 } 291 return result 292 } 293 294 func isIndexedChecked( 295 t *testing.T, 296 s client.Session, 297 ns ident.ID, 298 id ident.ID, 299 tags ident.TagIterator, 300 ) (bool, error) { 301 return isIndexedCheckedWithTime(t, s, ns, id, tags, xtime.Now()) 302 } 303 304 func isIndexedCheckedWithTime( 305 t *testing.T, 306 s client.Session, 307 ns ident.ID, 308 id ident.ID, 309 tags ident.TagIterator, 310 queryTime xtime.UnixNano, 311 ) (bool, error) { 312 q := newQuery(t, tags) 313 iter, _, err := s.FetchTaggedIDs(ContextWithDefaultTimeout(), ns, 314 index.Query{Query: q}, 315 index.QueryOptions{ 316 StartInclusive: queryTime, 317 EndExclusive: queryTime.Add(time.Nanosecond), 318 SeriesLimit: 10, 319 }) 320 if err != nil { 321 return false, err 322 } 323 324 defer iter.Finalize() 325 326 if !iter.Next() { 327 return false, nil 328 } 329 330 cuNs, cuID, cuTag := iter.Current() 331 if err := iter.Err(); err != nil { 332 return false, fmt.Errorf("iter err: %v", err) 333 } 334 335 if ns.String() != cuNs.String() { 336 return false, fmt.Errorf("namespace not matched") 337 } 338 if id.String() != cuID.String() { 339 return false, fmt.Errorf("id not matched") 340 } 341 if !ident.NewTagIterMatcher(tags).Matches(cuTag) { 342 return false, fmt.Errorf("tags did not match") 343 } 344 345 return true, nil 346 } 347 348 func newQuery(t *testing.T, tags ident.TagIterator) idx.Query { 349 tags = tags.Duplicate() 350 filters := make([]idx.Query, 0, tags.Remaining()) 351 for tags.Next() { 352 tag := tags.Current() 353 tq := idx.NewTermQuery(tag.Name.Bytes(), tag.Value.Bytes()) 354 filters = append(filters, tq) 355 } 356 return idx.NewConjunctionQuery(filters...) 357 } 358 359 // ContextWithDefaultTimeout returns a context with a default timeout 360 // set of one minute. 361 func ContextWithDefaultTimeout() context.Context { 362 ctx, _ := context.WithTimeout(context.Background(), time.Minute) //nolint 363 return ctx 364 }