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  }