github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/client/session_fetch_test.go (about)

     1  // Copyright (c) 2016 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  
    21  package client
    22  
    23  import (
    24  	"fmt"
    25  	"math"
    26  	"strconv"
    27  	"strings"
    28  	"sync"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/m3db/m3/src/dbnode/encoding"
    33  	"github.com/m3db/m3/src/dbnode/encoding/m3tsz"
    34  	"github.com/m3db/m3/src/dbnode/generated/thrift/rpc"
    35  	"github.com/m3db/m3/src/dbnode/namespace"
    36  	"github.com/m3db/m3/src/dbnode/topology"
    37  	"github.com/m3db/m3/src/dbnode/ts"
    38  	xmetrics "github.com/m3db/m3/src/dbnode/x/metrics"
    39  	"github.com/m3db/m3/src/x/checked"
    40  	xerrors "github.com/m3db/m3/src/x/errors"
    41  	"github.com/m3db/m3/src/x/ident"
    42  	xretry "github.com/m3db/m3/src/x/retry"
    43  	xtime "github.com/m3db/m3/src/x/time"
    44  
    45  	"github.com/golang/mock/gomock"
    46  	"github.com/stretchr/testify/assert"
    47  	"github.com/stretchr/testify/require"
    48  	"github.com/uber-go/tally"
    49  )
    50  
    51  const (
    52  	testNamespaceName = "testNs"
    53  )
    54  
    55  var (
    56  	fetchFailureErrStr = "a specific fetch error"
    57  )
    58  
    59  type testOptions struct {
    60  	nsID        ident.ID
    61  	opts        Options
    62  	encoderPool encoding.EncoderPool
    63  	setFetchAnn setFetchAnnotation
    64  	setWriteAnn setWriteAnnotation
    65  	annEqual    assertAnnotationEqual
    66  	expectedErr error
    67  }
    68  
    69  type testFetch struct {
    70  	id     string
    71  	values []testValue
    72  }
    73  
    74  type testFetches []testFetch
    75  
    76  func (f testFetches) IDs() []string {
    77  	var ids []string
    78  	for i := range f {
    79  		ids = append(ids, f[i].id)
    80  	}
    81  	return ids
    82  }
    83  
    84  func (f testFetches) IDsIter() ident.Iterator {
    85  	return ident.NewStringIDsSliceIterator(f.IDs())
    86  }
    87  
    88  type testValue struct {
    89  	value      float64
    90  	t          xtime.UnixNano
    91  	unit       xtime.Unit
    92  	annotation []byte
    93  }
    94  
    95  type testValuesByTime []testValue
    96  
    97  type setFetchAnnotation func([]testFetch) []testFetch
    98  type setWriteAnnotation func(*writeStub)
    99  type assertAnnotationEqual func(*testing.T, []byte, []byte)
   100  
   101  func (v testValuesByTime) Len() int      { return len(v) }
   102  func (v testValuesByTime) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
   103  func (v testValuesByTime) Less(i, j int) bool {
   104  	return v[i].t.Before(v[j].t)
   105  }
   106  
   107  type testFetchResultsAssertion struct {
   108  	trimToTimeRange int
   109  }
   110  
   111  func TestSessionFetchNotOpenError(t *testing.T) {
   112  	ctrl := gomock.NewController(t)
   113  	defer ctrl.Finish()
   114  
   115  	opts := newSessionTestOptions()
   116  	s, err := newSession(opts)
   117  	assert.NoError(t, err)
   118  
   119  	now := xtime.Now()
   120  	_, err = s.Fetch(ident.StringID("namespace"), ident.StringID("foo"), now.Add(-time.Hour), now)
   121  	assert.Error(t, err)
   122  	assert.Equal(t, ErrSessionStatusNotOpen, err)
   123  }
   124  
   125  func TestSessionFetchIDs(t *testing.T) {
   126  	opts := newSessionTestOptions()
   127  	testSessionFetchIDs(t, testOptions{nsID: ident.StringID(testNamespaceName), opts: opts})
   128  }
   129  
   130  func testSessionFetchIDs(t *testing.T, testOpts testOptions) {
   131  	ctrl := gomock.NewController(t)
   132  	defer ctrl.Finish()
   133  
   134  	nsID := testOpts.nsID
   135  	opts := testOpts.opts
   136  	opts = opts.SetFetchBatchSize(2)
   137  
   138  	s, err := newSession(opts)
   139  	require.NoError(t, err)
   140  	session := s.(*session)
   141  
   142  	start := xtime.Now().Truncate(time.Hour)
   143  	end := start.Add(2 * time.Hour)
   144  
   145  	fetches := testFetches([]testFetch{
   146  		{"foo", []testValue{
   147  			{1.0, start.Add(1 * time.Second), xtime.Second, []byte{1, 2, 3}},
   148  			{2.0, start.Add(2 * time.Second), xtime.Second, nil},
   149  			{3.0, start.Add(3 * time.Second), xtime.Second, nil},
   150  		}},
   151  		{"bar", []testValue{
   152  			{4.0, start.Add(1 * time.Second), xtime.Second, []byte{4, 5, 6}},
   153  			{5.0, start.Add(2 * time.Second), xtime.Second, nil},
   154  			{6.0, start.Add(3 * time.Second), xtime.Second, nil},
   155  		}},
   156  		{"baz", []testValue{
   157  			{7.0, start.Add(1 * time.Minute), xtime.Second, []byte{7, 8, 9}},
   158  			{8.0, start.Add(2 * time.Minute), xtime.Second, nil},
   159  			{9.0, start.Add(3 * time.Minute), xtime.Second, nil},
   160  		}},
   161  	})
   162  	if testOpts.setFetchAnn != nil {
   163  		fetches = testOpts.setFetchAnn(fetches)
   164  	}
   165  
   166  	expectedFetches := fetches
   167  	if testOpts.expectedErr != nil {
   168  		// If an error is expected then don't setup any go mock expectation for the host queues since
   169  		// they will never be fulfilled and will hang the test.
   170  		expectedFetches = nil
   171  	}
   172  	fetchBatchOps, enqueueWg := prepareTestFetchEnqueues(t, ctrl, session, expectedFetches)
   173  
   174  	valueWriteWg := sync.WaitGroup{}
   175  	valueWriteWg.Add(1)
   176  	go func() {
   177  		defer valueWriteWg.Done()
   178  		// Fulfill fetch ops once enqueued
   179  		enqueueWg.Wait()
   180  		fulfillFetchBatchOps(t, testOpts, fetches, *fetchBatchOps, 0)
   181  	}()
   182  
   183  	require.NoError(t, session.Open())
   184  
   185  	results, err := session.FetchIDs(nsID, fetches.IDsIter(), start, end)
   186  	if testOpts.expectedErr == nil {
   187  		require.NoError(t, err)
   188  		// wait for testValues to be written to before reading to assert.
   189  		valueWriteWg.Wait()
   190  		assertFetchResults(t, start, end, fetches, results, testOpts.annEqual)
   191  	} else {
   192  		require.Equal(t, testOpts.expectedErr, err)
   193  	}
   194  	require.NoError(t, session.Close())
   195  }
   196  
   197  func TestSessionFetchIDsWithRetries(t *testing.T) {
   198  	ctrl := gomock.NewController(t)
   199  	defer ctrl.Finish()
   200  
   201  	opts := newSessionTestOptions().
   202  		SetFetchBatchSize(2).
   203  		SetFetchRetrier(
   204  			xretry.NewRetrier(xretry.NewOptions().SetMaxRetries(1)))
   205  	testOpts := testOptions{nsID: ident.StringID(testNamespaceName), opts: opts}
   206  
   207  	s, err := newSession(opts)
   208  	assert.NoError(t, err)
   209  	session := s.(*session)
   210  
   211  	start := xtime.Now().Truncate(time.Hour)
   212  	end := start.Add(2 * time.Hour)
   213  
   214  	fetches := testFetches([]testFetch{
   215  		{"foo", []testValue{
   216  			{1.0, start.Add(1 * time.Second), xtime.Second, []byte{1, 2, 3}},
   217  			{2.0, start.Add(2 * time.Second), xtime.Second, nil},
   218  			{3.0, start.Add(3 * time.Second), xtime.Second, nil},
   219  		}},
   220  	})
   221  
   222  	successBatchOps, enqueueWg := prepareTestFetchEnqueuesWithErrors(t, ctrl, session, fetches)
   223  
   224  	go func() {
   225  		// Fulfill success fetch ops once all are enqueued
   226  		enqueueWg.Wait()
   227  		fulfillFetchBatchOps(t, testOpts, fetches, *successBatchOps, 0)
   228  	}()
   229  
   230  	assert.NoError(t, session.Open())
   231  
   232  	results, err := session.FetchIDs(testOpts.nsID, fetches.IDsIter(), start, end)
   233  	assert.NoError(t, err)
   234  	assertFetchResults(t, start, end, fetches, results, nil)
   235  
   236  	assert.NoError(t, session.Close())
   237  }
   238  
   239  func TestSessionFetchIDsTrimsWindowsInTimeWindow(t *testing.T) {
   240  	ctrl := gomock.NewController(t)
   241  	defer ctrl.Finish()
   242  
   243  	opts := newSessionTestOptions().SetFetchBatchSize(2)
   244  	testOpts := testOptions{nsID: ident.StringID(testNamespaceName), opts: opts}
   245  
   246  	s, err := newSession(opts)
   247  	assert.NoError(t, err)
   248  	session := s.(*session)
   249  
   250  	start := xtime.Now().Truncate(time.Hour)
   251  	end := start.Add(2 * time.Hour)
   252  
   253  	fetches := testFetches([]testFetch{
   254  		{"foo", []testValue{
   255  			{0.0, start.Add(-1 * time.Second), xtime.Second, nil},
   256  			{1.0, start.Add(1 * time.Second), xtime.Second, []byte{1, 2, 3}},
   257  			{2.0, start.Add(2 * time.Second), xtime.Second, nil},
   258  			{3.0, start.Add(3 * time.Second), xtime.Second, nil},
   259  			{4.0, end.Add(1 * time.Second), xtime.Second, nil},
   260  		}},
   261  	})
   262  
   263  	fetchBatchOps, enqueueWg := prepareTestFetchEnqueues(t, ctrl, session, fetches)
   264  
   265  	go func() {
   266  		// Fulfill fetch ops once enqueued
   267  		enqueueWg.Wait()
   268  		fulfillFetchBatchOps(t, testOpts, fetches, *fetchBatchOps, 0)
   269  	}()
   270  
   271  	assert.NoError(t, session.Open())
   272  
   273  	result, err := session.Fetch(testOpts.nsID, ident.StringID(fetches[0].id), start, end)
   274  	assert.NoError(t, err)
   275  
   276  	results := encoding.NewSeriesIterators([]encoding.SeriesIterator{result})
   277  	assertion := assertFetchResults(t, start, end, fetches, results, nil)
   278  	assert.Equal(t, 2, assertion.trimToTimeRange)
   279  
   280  	assert.NoError(t, session.Close())
   281  }
   282  
   283  func TestSessionFetchIDsBadRequestErrorIsNonRetryable(t *testing.T) {
   284  	ctrl := gomock.NewController(t)
   285  	defer ctrl.Finish()
   286  
   287  	opts := newSessionTestOptions()
   288  	s, err := newSession(opts)
   289  	assert.NoError(t, err)
   290  	session := s.(*session)
   291  
   292  	start := xtime.Now().Truncate(time.Hour)
   293  	end := start.Add(2 * time.Hour)
   294  
   295  	mockHostQueues(ctrl, session, sessionTestReplicas, []testEnqueueFn{
   296  		func(idx int, op op) {
   297  			go func() {
   298  				op.CompletionFn()(nil, &rpc.Error{
   299  					Type:    rpc.ErrorType_BAD_REQUEST,
   300  					Message: "expected bad request error",
   301  				})
   302  			}()
   303  		},
   304  	})
   305  
   306  	assert.NoError(t, session.Open())
   307  
   308  	_, err = session.FetchIDs(
   309  		ident.StringID(testNamespaceName),
   310  		ident.NewIDsIterator(ident.StringID("foo"), ident.StringID("bar")), start, end)
   311  	assert.Error(t, err)
   312  	assert.True(t, xerrors.IsNonRetryableError(err))
   313  
   314  	assert.NoError(t, session.Close())
   315  }
   316  
   317  func TestSessionFetchReadConsistencyLevelAll(t *testing.T) {
   318  	ctrl := gomock.NewController(t)
   319  	defer ctrl.Finish()
   320  
   321  	testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelAll, 0, outcomeSuccess)
   322  	for i := 1; i <= 3; i++ {
   323  		testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelAll, i, outcomeFail)
   324  	}
   325  }
   326  
   327  func TestSessionFetchReadConsistencyLevelUnstrictAll(t *testing.T) {
   328  	ctrl := gomock.NewController(t)
   329  	defer ctrl.Finish()
   330  
   331  	for i := 0; i <= 2; i++ {
   332  		testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelUnstrictAll, i, outcomeSuccess)
   333  	}
   334  	testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelUnstrictAll, 3, outcomeFail)
   335  }
   336  
   337  func TestSessionFetchReadConsistencyLevelMajority(t *testing.T) {
   338  	ctrl := gomock.NewController(t)
   339  	defer ctrl.Finish()
   340  
   341  	for i := 0; i <= 1; i++ {
   342  		testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelMajority, i, outcomeSuccess)
   343  	}
   344  	for i := 2; i <= 3; i++ {
   345  		testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelMajority, i, outcomeFail)
   346  	}
   347  }
   348  
   349  func TestSessionFetchReadConsistencyLevelUnstrictMajority(t *testing.T) {
   350  	ctrl := gomock.NewController(t)
   351  	defer ctrl.Finish()
   352  
   353  	for i := 0; i <= 2; i++ {
   354  		testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelUnstrictMajority, i, outcomeSuccess)
   355  	}
   356  	testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelUnstrictMajority, 3, outcomeFail)
   357  }
   358  
   359  func TestSessionFetchReadConsistencyLevelOne(t *testing.T) {
   360  	ctrl := gomock.NewController(t)
   361  	defer ctrl.Finish()
   362  
   363  	for i := 0; i <= 2; i++ {
   364  		testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelOne, i, outcomeSuccess)
   365  	}
   366  	testFetchConsistencyLevel(t, ctrl, topology.ReadConsistencyLevelOne, 3, outcomeFail)
   367  }
   368  
   369  func testFetchConsistencyLevel(
   370  	t *testing.T,
   371  	ctrl *gomock.Controller,
   372  	level topology.ReadConsistencyLevel,
   373  	failures int,
   374  	expected outcome,
   375  ) {
   376  	opts := newSessionTestOptions()
   377  	opts = opts.SetReadConsistencyLevel(level)
   378  
   379  	reporterOpts := xmetrics.NewTestStatsReporterOptions().
   380  		SetCaptureEvents(true)
   381  	reporter := xmetrics.NewTestStatsReporter(reporterOpts)
   382  	scope, closer := tally.NewRootScope(tally.ScopeOptions{Reporter: reporter}, time.Millisecond)
   383  	defer closer.Close()
   384  
   385  	opts = opts.SetInstrumentOptions(opts.InstrumentOptions().
   386  		SetMetricsScope(scope))
   387  
   388  	testOpts := testOptions{nsID: ident.StringID(testNamespaceName), opts: opts}
   389  
   390  	s, err := newSession(opts)
   391  	assert.NoError(t, err)
   392  	session := s.(*session)
   393  
   394  	start := xtime.Now().Truncate(time.Hour)
   395  	end := start.Add(2 * time.Hour)
   396  
   397  	fetches := testFetches([]testFetch{
   398  		{"foo", []testValue{
   399  			{1.0, start.Add(1 * time.Second), xtime.Second, []byte{1, 2, 3}},
   400  			{2.0, start.Add(2 * time.Second), xtime.Second, nil},
   401  			{3.0, start.Add(3 * time.Second), xtime.Second, nil},
   402  		}},
   403  	})
   404  
   405  	fetchBatchOps, enqueueWg := prepareTestFetchEnqueues(t, ctrl, session, fetches)
   406  
   407  	go func() {
   408  		// Fulfill fetch ops once enqueued
   409  		enqueueWg.Wait()
   410  		fulfillFetchBatchOps(t, testOpts, fetches, *fetchBatchOps, failures)
   411  	}()
   412  
   413  	assert.NoError(t, session.Open())
   414  
   415  	results, err := session.FetchIDs(ident.StringID(testNamespaceName),
   416  		fetches.IDsIter(), start, end)
   417  	if expected == outcomeSuccess {
   418  		assert.NoError(t, err)
   419  		assertFetchResults(t, start, end, fetches, results, nil)
   420  	} else {
   421  		assert.Error(t, err)
   422  		assert.True(t, IsInternalServerError(err))
   423  		assert.False(t, IsBadRequestError(err))
   424  		resultErrStr := fmt.Sprintf("%v", err)
   425  		assert.True(t, strings.Contains(resultErrStr,
   426  			fmt.Sprintf("failed to meet consistency level %s with %d/3 success", level.String(), 3-failures)))
   427  		assert.True(t, strings.Contains(resultErrStr,
   428  			fetchFailureErrStr))
   429  	}
   430  
   431  	assert.NoError(t, session.Close())
   432  
   433  	counters := reporter.Counters()
   434  	for counters["fetch.success"] == 0 && counters["fetch.errors"] == 0 {
   435  		time.Sleep(time.Millisecond)
   436  		counters = reporter.Counters()
   437  	}
   438  	if expected == outcomeSuccess {
   439  		assert.Equal(t, 1, int(counters["fetch.success"]))
   440  		assert.Equal(t, 0, int(counters["fetch.errors"]))
   441  	} else {
   442  		assert.Equal(t, 0, int(counters["fetch.success"]))
   443  		assert.Equal(t, 1, int(counters["fetch.errors"]))
   444  	}
   445  	if failures > 0 {
   446  		for _, event := range reporter.Events() {
   447  			if event.Name() == "fetch.nodes-responding-error" {
   448  				nodesFailing, convErr := strconv.Atoi(event.Tags()["nodes"])
   449  				require.NoError(t, convErr)
   450  				assert.True(t, 0 < nodesFailing && nodesFailing <= failures)
   451  				assert.Equal(t, int64(1), event.Value())
   452  				break
   453  			}
   454  		}
   455  	}
   456  }
   457  
   458  func prepareTestFetchEnqueuesWithErrors(
   459  	t *testing.T,
   460  	ctrl *gomock.Controller,
   461  	session *session,
   462  	fetches []testFetch,
   463  ) (*[]*fetchBatchOp, *sync.WaitGroup) {
   464  	failureEnqueueFn := func(idx int, op op) {
   465  		fetch, ok := op.(*fetchBatchOp)
   466  		assert.True(t, ok)
   467  		go func() {
   468  			fetch.CompletionFn()(nil, fmt.Errorf("random failure"))
   469  		}()
   470  	}
   471  
   472  	var successBatchOps []*fetchBatchOp
   473  	successEnqueueFn := func(idx int, op op) {
   474  		fetch, ok := op.(*fetchBatchOp)
   475  		assert.True(t, ok)
   476  		successBatchOps = append(successBatchOps, fetch)
   477  	}
   478  
   479  	var enqueueFns []testEnqueueFn
   480  	fetchBatchSize := session.opts.FetchBatchSize()
   481  	for i := 0; i < int(math.Ceil(float64(len(fetches))/float64(fetchBatchSize))); i++ {
   482  		enqueueFns = append(enqueueFns, failureEnqueueFn)
   483  		enqueueFns = append(enqueueFns, successEnqueueFn)
   484  	}
   485  
   486  	enqueueWg := mockHostQueues(ctrl, session, sessionTestReplicas, enqueueFns)
   487  	return &successBatchOps, enqueueWg
   488  }
   489  
   490  func prepareTestFetchEnqueues(
   491  	t *testing.T,
   492  	ctrl *gomock.Controller,
   493  	session *session,
   494  	fetches []testFetch,
   495  ) (*[]*fetchBatchOp, *sync.WaitGroup) {
   496  	var fetchBatchOps []*fetchBatchOp
   497  	enqueueFn := func(idx int, op op) {
   498  		fetch, ok := op.(*fetchBatchOp)
   499  		assert.True(t, ok)
   500  		fetchBatchOps = append(fetchBatchOps, fetch)
   501  	}
   502  
   503  	var enqueueFns []testEnqueueFn
   504  	fetchBatchSize := session.opts.FetchBatchSize()
   505  	for i := 0; i < int(math.Ceil(float64(len(fetches))/float64(fetchBatchSize))); i++ {
   506  		enqueueFns = append(enqueueFns, enqueueFn)
   507  	}
   508  	enqueueWg := mockHostQueues(ctrl, session, sessionTestReplicas, enqueueFns)
   509  	return &fetchBatchOps, enqueueWg
   510  }
   511  
   512  func fulfillFetchBatchOps(
   513  	t *testing.T,
   514  	testOpts testOptions,
   515  	fetches []testFetch,
   516  	fetchBatchOps []*fetchBatchOp,
   517  	failures int,
   518  ) {
   519  	failed := make(map[string]int)
   520  	for _, f := range fetches {
   521  		failed[f.id] = 0
   522  	}
   523  
   524  	for _, op := range fetchBatchOps {
   525  		for i, id := range op.request.Ids {
   526  			calledCompletionFn := false
   527  			for _, f := range fetches {
   528  				if f.id != string(id) {
   529  					continue
   530  				}
   531  
   532  				if failed[f.id] < failures {
   533  					// Requires failing
   534  					failed[f.id] = failed[f.id] + 1
   535  					op.completionFns[i](nil, &rpc.Error{
   536  						Type:    rpc.ErrorType_INTERNAL_ERROR,
   537  						Message: fetchFailureErrStr,
   538  					})
   539  					calledCompletionFn = true
   540  					break
   541  				}
   542  
   543  				var encoder encoding.Encoder
   544  				if testOpts.encoderPool == nil {
   545  					encoder = m3tsz.NewEncoder(f.values[0].t, nil, true, nil)
   546  				} else {
   547  					encoder = testOpts.encoderPool.Get()
   548  					nsCtx := namespace.NewContextFor(testOpts.nsID, testOpts.opts.SchemaRegistry())
   549  					encoder.Reset(f.values[0].t, 0, nsCtx.Schema)
   550  				}
   551  				for _, value := range f.values {
   552  					dp := ts.Datapoint{
   553  						TimestampNanos: value.t,
   554  						Value:          value.value,
   555  					}
   556  					encoder.Encode(dp, value.unit, value.annotation)
   557  				}
   558  				seg := encoder.Discard()
   559  				op.completionFns[i]([]*rpc.Segments{{
   560  					Merged: &rpc.Segment{Head: bytesIfNotNil(seg.Head), Tail: bytesIfNotNil(seg.Tail)},
   561  				}}, nil)
   562  				calledCompletionFn = true
   563  				break
   564  			}
   565  			assert.True(t, calledCompletionFn)
   566  		}
   567  	}
   568  }
   569  
   570  func bytesIfNotNil(data checked.Bytes) []byte {
   571  	if data != nil {
   572  		return data.Bytes()
   573  	}
   574  	return nil
   575  }
   576  
   577  func assertFetchResults(
   578  	t *testing.T,
   579  	start, end xtime.UnixNano,
   580  	fetches []testFetch,
   581  	results encoding.SeriesIterators,
   582  	annEqual assertAnnotationEqual,
   583  ) testFetchResultsAssertion {
   584  	trimToTimeRange := 0
   585  	assert.Equal(t, len(fetches), results.Len())
   586  
   587  	for i, series := range results.Iters() {
   588  		expected := fetches[i]
   589  		assert.Equal(t, expected.id, series.ID().String())
   590  		assert.Equal(t, start, series.Start())
   591  		assert.Equal(t, end, series.End())
   592  
   593  		// Trim the expected values to the start/end window
   594  		var expectedValues []testValue
   595  		for j := range expected.values {
   596  			ts := expected.values[j].t
   597  			if ts.Before(start) {
   598  				trimToTimeRange++
   599  				continue
   600  			}
   601  			if ts.Equal(end) || ts.After(end) {
   602  				trimToTimeRange++
   603  				continue
   604  			}
   605  			expectedValues = append(expectedValues, expected.values[j])
   606  		}
   607  
   608  		j := 0
   609  		for series.Next() {
   610  			value := expectedValues[j]
   611  			dp, unit, annotation := series.Current()
   612  
   613  			assert.Equal(t, value.t, dp.TimestampNanos)
   614  			assert.Equal(t, value.value, dp.Value)
   615  			assert.Equal(t, value.unit, unit)
   616  			if annEqual != nil {
   617  				annEqual(t, value.annotation, annotation)
   618  			} else {
   619  				assert.Equal(t, value.annotation, []byte(annotation))
   620  			}
   621  			j++
   622  		}
   623  		assert.Equal(t, len(expectedValues), j)
   624  		assert.NoError(t, series.Err())
   625  	}
   626  	results.Close()
   627  
   628  	return testFetchResultsAssertion{trimToTimeRange: trimToTimeRange}
   629  }