github.com/m3db/m3@v1.5.0/src/query/storage/fanout/storage_test.go (about)

     1  //
     2  // Copyright (c) 2018 Uber Technologies, Inc.
     3  //
     4  // Permission is hereby granted, free of charge, to any person obtaining a copy
     5  // of this software and associated documentation files (the "Software"), to deal
     6  // in the Software without restriction, including without limitation the rights
     7  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     8  // copies of the Software, and to permit persons to whom the Software is
     9  // furnished to do so, subject to the following conditions:
    10  //
    11  // The above copyright notice and this permission notice shall be included in
    12  // all copies or substantial portions of the Software.
    13  //
    14  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    15  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    16  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    17  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    18  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    19  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    20  // THE SOFTWARE.
    21  
    22  package fanout
    23  
    24  import (
    25  	"context"
    26  	"errors"
    27  	"fmt"
    28  	"sort"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/m3db/m3/src/dbnode/client"
    33  	"github.com/m3db/m3/src/dbnode/encoding"
    34  	"github.com/m3db/m3/src/query/block"
    35  	errs "github.com/m3db/m3/src/query/errors"
    36  	"github.com/m3db/m3/src/query/models"
    37  	"github.com/m3db/m3/src/query/policy/filter"
    38  	"github.com/m3db/m3/src/query/storage"
    39  	storagem3 "github.com/m3db/m3/src/query/storage/m3"
    40  	"github.com/m3db/m3/src/query/storage/m3/consolidators"
    41  	"github.com/m3db/m3/src/query/storage/m3/storagemetadata"
    42  	"github.com/m3db/m3/src/query/test"
    43  	"github.com/m3db/m3/src/query/test/m3"
    44  	"github.com/m3db/m3/src/query/test/seriesiter"
    45  	"github.com/m3db/m3/src/query/ts"
    46  	"github.com/m3db/m3/src/x/ident"
    47  	"github.com/m3db/m3/src/x/instrument"
    48  	xtest "github.com/m3db/m3/src/x/test"
    49  	xtime "github.com/m3db/m3/src/x/time"
    50  
    51  	"github.com/golang/mock/gomock"
    52  	"github.com/stretchr/testify/assert"
    53  	"github.com/stretchr/testify/require"
    54  )
    55  
    56  func filterFunc(output bool) filter.Storage {
    57  	return func(query storage.Query, store storage.Storage) bool {
    58  		return output
    59  	}
    60  }
    61  
    62  func filterCompleteTagsFunc(output bool) filter.StorageCompleteTags {
    63  	return func(query storage.CompleteTagsQuery, store storage.Storage) bool {
    64  		return output
    65  	}
    66  }
    67  
    68  func fakeIterator(t *testing.T) encoding.SeriesIterators {
    69  	id := ident.StringID("id")
    70  	namespace := ident.StringID("metrics")
    71  	return encoding.NewSeriesIterators([]encoding.SeriesIterator{
    72  		encoding.NewSeriesIterator(encoding.SeriesIteratorOptions{
    73  			ID:        id,
    74  			Namespace: namespace,
    75  			Tags: seriesiter.GenerateSingleSampleTagIterator(
    76  				xtest.NewController(t), seriesiter.GenerateTag()),
    77  		}, nil),
    78  	})
    79  }
    80  
    81  type fetchResponse struct {
    82  	result encoding.SeriesIterators
    83  	err    error
    84  }
    85  
    86  func setupFanoutRead(t *testing.T, output bool, response ...*fetchResponse) storage.Storage {
    87  	if len(response) == 0 {
    88  		response = []*fetchResponse{{err: fmt.Errorf("unable to get response")}}
    89  	}
    90  
    91  	ctrl := xtest.NewController(t)
    92  	store1, session1 := m3.NewStorageAndSession(t, ctrl)
    93  	store2, session2 := m3.NewStorageAndSession(t, ctrl)
    94  	session1.EXPECT().FetchTagged(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
    95  		Return(response[0].result, client.FetchResponseMetadata{Exhaustive: true}, response[0].err)
    96  	session2.EXPECT().FetchTagged(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
    97  		Return(response[len(response)-1].result, client.FetchResponseMetadata{Exhaustive: true}, response[len(response)-1].err)
    98  	session1.EXPECT().FetchTaggedIDs(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
    99  		Return(nil, client.FetchResponseMetadata{Exhaustive: false}, errs.ErrNotImplemented)
   100  	session2.EXPECT().FetchTaggedIDs(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
   101  		Return(nil, client.FetchResponseMetadata{Exhaustive: false}, errs.ErrNotImplemented)
   102  
   103  	stores := []storage.Storage{
   104  		store1, store2,
   105  	}
   106  
   107  	store := NewStorage(stores, filterFunc(output), filterFunc(output),
   108  		filterCompleteTagsFunc(output), models.NewTagOptions(),
   109  		storagem3.NewOptions(encoding.NewOptions()), instrument.NewOptions())
   110  	return store
   111  }
   112  
   113  func setupFanoutWrite(t *testing.T, output bool, errs ...error) storage.Storage {
   114  	ctrl := xtest.NewController(t)
   115  	store1, session1 := m3.NewStorageAndSession(t, ctrl)
   116  	store2, session2 := m3.NewStorageAndSession(t, ctrl)
   117  	session1.EXPECT().
   118  		WriteTagged(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(),
   119  			gomock.Any(), gomock.Any(), gomock.Any()).Return(errs[0])
   120  	session1.EXPECT().IteratorPools().
   121  		Return(nil, nil).AnyTimes()
   122  	session1.EXPECT().FetchTaggedIDs(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
   123  		Return(nil, client.FetchResponseMetadata{Exhaustive: true}, errs[0]).AnyTimes()
   124  	session1.EXPECT().Aggregate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
   125  		Return(nil, client.FetchResponseMetadata{Exhaustive: true}, errs[0]).AnyTimes()
   126  
   127  	session2.EXPECT().
   128  		WriteTagged(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(),
   129  			gomock.Any(), gomock.Any(), gomock.Any()).Return(errs[len(errs)-1])
   130  	session2.EXPECT().IteratorPools().
   131  		Return(nil, nil).AnyTimes()
   132  
   133  	stores := []storage.Storage{
   134  		store1, store2,
   135  	}
   136  	store := NewStorage(stores, filterFunc(output), filterFunc(output),
   137  		filterCompleteTagsFunc(output), models.NewTagOptions(),
   138  		storagem3.NewOptions(encoding.NewOptions()), instrument.NewOptions())
   139  	return store
   140  }
   141  
   142  func TestCompleteTags_RestrictOptionsWorks_SingleStore(t *testing.T) {
   143  	ctrl := xtest.NewController(t)
   144  	store := storage.NewMockStorage(ctrl)
   145  
   146  	meta := block.NewResultMetadata()
   147  	meta.Exhaustive = false
   148  	fullResult := &consolidators.CompleteTagsResult{
   149  		CompleteNameOnly: false,
   150  		CompletedTags: []consolidators.CompletedTag{
   151  			{Name: []byte("bar"), Values: xtest.BytesArray("zulu", "quail")},
   152  			{Name: []byte("foo"), Values: xtest.BytesArray("quail")},
   153  		},
   154  
   155  		Metadata: meta,
   156  	}
   157  
   158  	store.EXPECT().CompleteTags(gomock.Any(), gomock.Any(), gomock.Any()).Return(fullResult, nil)
   159  
   160  	stores := []storage.Storage{store}
   161  	fanoutStorage := NewStorage(stores, filterFunc(false), filterFunc(false),
   162  		filterCompleteTagsFunc(true), models.NewTagOptions(),
   163  		storagem3.NewOptions(encoding.NewOptions()), instrument.NewOptions())
   164  
   165  	fetchOptions := storage.NewFetchOptions()
   166  	fetchOptions.RestrictQueryOptions = &storage.RestrictQueryOptions{
   167  		RestrictByTag: &storage.RestrictByTag{
   168  			Strip: xtest.BytesArray("bar"),
   169  		},
   170  	}
   171  
   172  	completeTagsResult, err := fanoutStorage.CompleteTags(
   173  		context.TODO(),
   174  		&storage.CompleteTagsQuery{
   175  			CompleteNameOnly: true,
   176  			TagMatchers:      models.Matchers{},
   177  		},
   178  		fetchOptions)
   179  
   180  	require.NoError(t, err)
   181  
   182  	actualTags := completeTagsResult.CompletedTags
   183  	require.Len(t, actualTags, 1)
   184  	require.Equal(t, []byte("foo"), actualTags[0].Name)
   185  }
   186  
   187  func TestQueryStorageMetadataAttributes(t *testing.T) {
   188  	ctrl := xtest.NewController(t)
   189  	store1, _ := m3.NewStorageAndSession(t, ctrl)
   190  
   191  	stores := []storage.Storage{store1}
   192  
   193  	store := NewStorage(stores, filterFunc(false), filterFunc(false),
   194  		filterCompleteTagsFunc(false), models.NewTagOptions(),
   195  		storagem3.NewOptions(encoding.NewOptions()), instrument.NewOptions())
   196  
   197  	attrs, err := store.QueryStorageMetadataAttributes(
   198  		context.Background(),
   199  		time.Now().Add(-10*time.Minute),
   200  		time.Now(),
   201  		storage.NewFetchOptions(),
   202  	)
   203  	require.NoError(t, err)
   204  	require.Equal(t, []storagemetadata.Attributes{
   205  		{
   206  			MetricsType: storagemetadata.UnaggregatedMetricsType,
   207  			Retention:   m3.TestRetention,
   208  		},
   209  	}, attrs)
   210  }
   211  
   212  func TestQueryStorageMetadataAttributesMultipleStores(t *testing.T) {
   213  	ctrl := xtest.NewController(t)
   214  	aggNs1 := []storagem3.AggregatedClusterNamespaceDefinition{
   215  		{
   216  			NamespaceID: ident.StringID("5m:90d"),
   217  			Resolution:  5 * time.Minute,
   218  			Retention:   120 * 24 * time.Hour,
   219  		},
   220  	}
   221  	aggNs2 := []storagem3.AggregatedClusterNamespaceDefinition{
   222  		{
   223  			NamespaceID: ident.StringID("5m:90d"),
   224  			Resolution:  5 * time.Minute,
   225  			Retention:   120 * 24 * time.Hour,
   226  		},
   227  		{
   228  			NamespaceID: ident.StringID("10m:120d"),
   229  			Resolution:  10 * time.Minute,
   230  			Retention:   150 * 24 * time.Hour,
   231  		},
   232  	}
   233  	store1, _ := m3.NewStorageAndSessionWithAggregatedNamespaces(t, ctrl, aggNs1)
   234  	store2, _ := m3.NewStorageAndSessionWithAggregatedNamespaces(t, ctrl, aggNs2)
   235  
   236  	stores := []storage.Storage{store1, store2}
   237  
   238  	store := NewStorage(stores, filterFunc(false), filterFunc(false),
   239  		filterCompleteTagsFunc(false), models.NewTagOptions(),
   240  		storagem3.NewOptions(encoding.NewOptions()), instrument.NewOptions())
   241  
   242  	fetchOpts := storage.NewFetchOptions()
   243  	fetchOpts.FanoutOptions.FanoutAggregated = storage.FanoutForceEnable
   244  	attrs, err := store.QueryStorageMetadataAttributes(
   245  		context.Background(),
   246  		time.Now().Add(-200*24*time.Hour),
   247  		time.Now(),
   248  		fetchOpts,
   249  	)
   250  	require.NoError(t, err)
   251  
   252  	sort.Slice(attrs, func(i, j int) bool {
   253  		return attrs[i].Retention < attrs[j].Retention
   254  	})
   255  	require.Equal(t, []storagemetadata.Attributes{
   256  		{
   257  			MetricsType: storagemetadata.AggregatedMetricsType,
   258  			Resolution:  5 * time.Minute,
   259  			Retention:   120 * 24 * time.Hour,
   260  		},
   261  		{
   262  			MetricsType: storagemetadata.AggregatedMetricsType,
   263  			Resolution:  10 * time.Minute,
   264  			Retention:   150 * 24 * time.Hour,
   265  		},
   266  	}, attrs)
   267  }
   268  
   269  func TestFanoutReadEmpty(t *testing.T) {
   270  	store := setupFanoutRead(t, false)
   271  	res, err := store.FetchProm(context.TODO(), nil, storage.NewFetchOptions())
   272  	assert.NoError(t, err)
   273  	require.NotNil(t, res)
   274  	assert.Equal(t, 0, len(res.PromResult.GetTimeseries()))
   275  	assert.Equal(t, 0, res.Metadata.FetchedSeriesCount)
   276  	assert.Equal(t, block.ResultMetricMetadata{}, res.Metadata.MetadataByNameMerged())
   277  }
   278  
   279  func TestFanoutReadError(t *testing.T) {
   280  	store := setupFanoutRead(t, true)
   281  	opts := storage.NewFetchOptions()
   282  	_, err := store.FetchProm(context.TODO(), &storage.FetchQuery{}, opts)
   283  	assert.Error(t, err)
   284  }
   285  
   286  func TestFanoutReadSuccess(t *testing.T) {
   287  	store := setupFanoutRead(t, true, &fetchResponse{
   288  		result: fakeIterator(t),
   289  	},
   290  		&fetchResponse{result: fakeIterator(t)},
   291  	)
   292  	res, err := store.FetchProm(context.TODO(), &storage.FetchQuery{
   293  		Start: time.Now().Add(-time.Hour),
   294  		End:   time.Now(),
   295  	}, storage.NewFetchOptions())
   296  	require.NoError(t, err, "no error on read")
   297  	assert.NotNil(t, res)
   298  	assert.NoError(t, store.Close())
   299  }
   300  
   301  func TestFanoutSearchEmpty(t *testing.T) {
   302  	store := setupFanoutRead(t, false)
   303  	res, err := store.SearchSeries(context.TODO(), nil, nil)
   304  	assert.NoError(t, err, "No error")
   305  	require.NotNil(t, res, "Non empty result")
   306  	assert.Len(t, res.Metrics, 0, "No series")
   307  }
   308  
   309  func TestFanoutSearchError(t *testing.T) {
   310  	store := setupFanoutRead(t, true)
   311  	opts := storage.NewFetchOptions()
   312  	_, err := store.SearchSeries(context.TODO(), &storage.FetchQuery{}, opts)
   313  	assert.Error(t, err)
   314  }
   315  
   316  func TestFanoutWriteEmpty(t *testing.T) {
   317  	store := setupFanoutWrite(t, false, fmt.Errorf("write error"))
   318  	err := store.Write(context.TODO(), nil)
   319  	assert.NoError(t, err)
   320  }
   321  
   322  func TestFanoutWriteError(t *testing.T) {
   323  	store := setupFanoutWrite(t, true, fmt.Errorf("write error"))
   324  	datapoints := make(ts.Datapoints, 1)
   325  	datapoints[0] = ts.Datapoint{Timestamp: xtime.Now(), Value: 1}
   326  
   327  	writeQuery, err := storage.NewWriteQuery(storage.WriteQueryOptions{
   328  		Datapoints: datapoints,
   329  		Tags:       models.MustMakeTags("foo", "bar"),
   330  		Unit:       xtime.Second,
   331  	})
   332  	require.NoError(t, err)
   333  
   334  	assert.Error(t, store.Write(context.TODO(), writeQuery))
   335  }
   336  
   337  func TestFanoutWriteSuccess(t *testing.T) {
   338  	store := setupFanoutWrite(t, true, nil)
   339  	datapoints := make(ts.Datapoints, 1)
   340  	datapoints[0] = ts.Datapoint{Timestamp: xtime.Now(), Value: 1}
   341  
   342  	writeQuery, err := storage.NewWriteQuery(storage.WriteQueryOptions{
   343  		Datapoints: datapoints,
   344  		Tags:       models.MustMakeTags("foo", "bar"),
   345  		Unit:       xtime.Second,
   346  		Attributes: storagemetadata.Attributes{
   347  			MetricsType: storagemetadata.UnaggregatedMetricsType,
   348  		},
   349  	})
   350  	require.NoError(t, err)
   351  
   352  	assert.NoError(t, store.Write(context.TODO(), writeQuery))
   353  }
   354  
   355  func TestCompleteTagsError(t *testing.T) {
   356  	store := setupFanoutWrite(t, true, fmt.Errorf("err"))
   357  	datapoints := make(ts.Datapoints, 1)
   358  	datapoints[0] = ts.Datapoint{Timestamp: xtime.Now(), Value: 1}
   359  	_, err := store.CompleteTags(
   360  		context.TODO(),
   361  		&storage.CompleteTagsQuery{
   362  			CompleteNameOnly: true,
   363  			TagMatchers:      models.Matchers{},
   364  		},
   365  		storage.NewFetchOptions(),
   366  	)
   367  	assert.Error(t, err)
   368  }
   369  
   370  // Error continuation tests below.
   371  func TestFanoutSearchErrorContinues(t *testing.T) {
   372  	ctrl := xtest.NewController(t)
   373  	defer ctrl.Finish()
   374  
   375  	filter := func(_ storage.Query, _ storage.Storage) bool { return true }
   376  	tFilter := func(_ storage.CompleteTagsQuery, _ storage.Storage) bool { return true }
   377  	okStore := storage.NewMockStorage(ctrl)
   378  	okStore.EXPECT().SearchSeries(gomock.Any(), gomock.Any(), gomock.Any()).
   379  		Return(
   380  			&storage.SearchResults{
   381  				Metrics: models.Metrics{
   382  					models.Metric{
   383  						ID: []byte("ok"),
   384  						Tags: models.NewTags(1, models.NewTagOptions()).AddTag(models.Tag{
   385  							Name:  []byte("foo"),
   386  							Value: []byte("bar"),
   387  						}),
   388  					},
   389  				},
   390  			},
   391  			nil,
   392  		)
   393  
   394  	dupeStore := storage.NewMockStorage(ctrl)
   395  	dupeStore.EXPECT().SearchSeries(gomock.Any(), gomock.Any(), gomock.Any()).
   396  		Return(
   397  			&storage.SearchResults{
   398  				Metrics: models.Metrics{
   399  					models.Metric{
   400  						ID: []byte("ok"),
   401  						Tags: models.NewTags(1, models.NewTagOptions()).AddTag(models.Tag{
   402  							Name:  []byte("foo"),
   403  							Value: []byte("bar"),
   404  						}),
   405  					},
   406  				},
   407  			},
   408  			nil,
   409  		)
   410  
   411  	warnStore := storage.NewMockStorage(ctrl)
   412  	warnStore.EXPECT().SearchSeries(gomock.Any(), gomock.Any(), gomock.Any()).
   413  		Return(
   414  			&storage.SearchResults{
   415  				Metrics: models.Metrics{
   416  					models.Metric{
   417  						ID: []byte("warn"),
   418  					},
   419  				},
   420  			},
   421  			errors.New("e"),
   422  		)
   423  	warnStore.EXPECT().ErrorBehavior().Return(storage.BehaviorWarn)
   424  	warnStore.EXPECT().Name().Return("warn").AnyTimes()
   425  
   426  	stores := []storage.Storage{warnStore, okStore, dupeStore}
   427  	store := NewStorage(stores, filter, filter, tFilter,
   428  		models.NewTagOptions(), storagem3.NewOptions(encoding.NewOptions()),
   429  		instrument.NewOptions())
   430  	opts := storage.NewFetchOptions()
   431  	result, err := store.SearchSeries(context.TODO(), &storage.FetchQuery{}, opts)
   432  	assert.NoError(t, err)
   433  
   434  	require.Equal(t, 1, len(result.Metrics))
   435  	assert.Equal(t, []byte("ok"), result.Metrics[0].ID)
   436  	require.Equal(t, 1, result.Metrics[0].Tags.Len())
   437  	tag := result.Metrics[0].Tags.Tags[0]
   438  	require.Equal(t, []byte("foo"), tag.Name)
   439  	require.Equal(t, []byte("bar"), tag.Value)
   440  }
   441  
   442  func TestFanoutCompleteTagsErrorContinues(t *testing.T) {
   443  	ctrl := xtest.NewController(t)
   444  	defer ctrl.Finish()
   445  
   446  	filter := func(_ storage.Query, _ storage.Storage) bool { return true }
   447  	tFilter := func(_ storage.CompleteTagsQuery, _ storage.Storage) bool { return true }
   448  	okStore := storage.NewMockStorage(ctrl)
   449  	okStore.EXPECT().CompleteTags(gomock.Any(), gomock.Any(), gomock.Any()).
   450  		Return(
   451  			&consolidators.CompleteTagsResult{
   452  				CompleteNameOnly: true,
   453  				CompletedTags: []consolidators.CompletedTag{
   454  					{
   455  						Name: []byte("ok"),
   456  					},
   457  				},
   458  			},
   459  			nil,
   460  		)
   461  
   462  	warnStore := storage.NewMockStorage(ctrl)
   463  	warnStore.EXPECT().CompleteTags(gomock.Any(), gomock.Any(), gomock.Any()).
   464  		Return(
   465  			&consolidators.CompleteTagsResult{
   466  				CompleteNameOnly: true,
   467  				CompletedTags: []consolidators.CompletedTag{
   468  					{
   469  						Name: []byte("warn"),
   470  					},
   471  				},
   472  			},
   473  			errors.New("e"),
   474  		)
   475  	warnStore.EXPECT().ErrorBehavior().Return(storage.BehaviorWarn)
   476  	warnStore.EXPECT().Name().Return("warn").AnyTimes()
   477  
   478  	stores := []storage.Storage{warnStore, okStore}
   479  	store := NewStorage(stores, filter, filter, tFilter,
   480  		models.NewTagOptions(), storagem3.NewOptions(encoding.NewOptions()),
   481  		instrument.NewOptions())
   482  	opts := storage.NewFetchOptions()
   483  	q := &storage.CompleteTagsQuery{CompleteNameOnly: true}
   484  	result, err := store.CompleteTags(context.TODO(), q, opts)
   485  	assert.NoError(t, err)
   486  
   487  	require.True(t, result.CompleteNameOnly)
   488  	require.Equal(t, 1, len(result.CompletedTags))
   489  	assert.Equal(t, []byte("ok"), result.CompletedTags[0].Name)
   490  }
   491  
   492  func TestFanoutFetchBlocksErrorContinues(t *testing.T) {
   493  	ctrl := xtest.NewController(t)
   494  	defer ctrl.Finish()
   495  
   496  	filter := func(_ storage.Query, _ storage.Storage) bool { return true }
   497  	tFilter := func(_ storage.CompleteTagsQuery, _ storage.Storage) bool { return true }
   498  	okBlock := block.NewScalar(1, block.Metadata{})
   499  	okStore := storage.NewMockStorage(ctrl)
   500  	okStore.EXPECT().FetchBlocks(gomock.Any(), gomock.Any(), gomock.Any()).
   501  		Return(
   502  			block.Result{
   503  				Blocks: []block.Block{okBlock},
   504  			},
   505  			nil,
   506  		)
   507  
   508  	warnStore := storage.NewMockStorage(ctrl)
   509  	warnBlock := block.NewScalar(2, block.Metadata{})
   510  	warnStore.EXPECT().FetchBlocks(gomock.Any(), gomock.Any(), gomock.Any()).
   511  		Return(
   512  			block.Result{
   513  				Blocks: []block.Block{warnBlock},
   514  			},
   515  			errors.New("e"),
   516  		)
   517  	warnStore.EXPECT().ErrorBehavior().Return(storage.BehaviorWarn)
   518  	warnStore.EXPECT().Name().Return("warn").AnyTimes()
   519  
   520  	stores := []storage.Storage{okStore, warnStore}
   521  	store := NewStorage(stores, filter, filter, tFilter,
   522  		models.NewTagOptions(), storagem3.NewOptions(encoding.NewOptions()),
   523  		instrument.NewOptions())
   524  	opts := storage.NewFetchOptions()
   525  	result, err := store.FetchBlocks(context.TODO(), &storage.FetchQuery{}, opts)
   526  	assert.NoError(t, err)
   527  
   528  	require.Equal(t, 1, len(result.Blocks))
   529  	assert.Equal(t, block.BlockLazy, result.Blocks[0].Info().Type())
   530  	it, err := result.Blocks[0].StepIter()
   531  	require.NoError(t, err)
   532  	for it.Next() {
   533  		assert.Equal(t, []float64{1}, it.Current().Values())
   534  	}
   535  }
   536  
   537  func TestFanoutFetchErrorContinues(t *testing.T) {
   538  	ctrl := xtest.NewController(t)
   539  	defer ctrl.Finish()
   540  
   541  	filter := func(_ storage.Query, _ storage.Storage) bool {
   542  		return true
   543  	}
   544  
   545  	tFilter := func(_ storage.CompleteTagsQuery, _ storage.Storage) bool {
   546  		return true
   547  	}
   548  
   549  	okStore := storage.NewMockStorage(ctrl)
   550  	okStore.EXPECT().FetchCompressed(gomock.Any(), gomock.Any(), gomock.Any()).
   551  		Return(
   552  			fetchResult("ok"),
   553  			nil,
   554  		).AnyTimes()
   555  	okStore.EXPECT().Type().Return(storage.TypeLocalDC).AnyTimes()
   556  
   557  	warnStore := storage.NewMockStorage(ctrl)
   558  	warnStore.EXPECT().FetchCompressed(gomock.Any(), gomock.Any(), gomock.Any()).
   559  		Return(
   560  			fetchResult("warn"),
   561  			errors.New("e"),
   562  		)
   563  	warnStore.EXPECT().ErrorBehavior().Return(storage.BehaviorWarn)
   564  	warnStore.EXPECT().Name().Return("warn").AnyTimes()
   565  
   566  	stores := []storage.Storage{okStore, warnStore}
   567  	store := NewStorage(stores, filter, filter, tFilter,
   568  		models.NewTagOptions(), storagem3.NewOptions(encoding.NewOptions()),
   569  		instrument.NewOptions())
   570  	opts := storage.NewFetchOptions()
   571  	opts.SeriesLimit = 300
   572  	result, err := store.FetchProm(context.TODO(), &storage.FetchQuery{}, opts)
   573  	assert.NoError(t, err)
   574  
   575  	series := result.PromResult.GetTimeseries()
   576  	require.Equal(t, 2, len(series))
   577  }
   578  
   579  func fetchResult(name string) consolidators.MultiFetchResult {
   580  	it, _ := test.BuildTestSeriesIterator(name)
   581  	iters := encoding.NewSeriesIterators([]encoding.SeriesIterator{it})
   582  	result := consolidators.NewMultiFetchResult(
   583  		consolidators.NamespaceCoversAllQueryRange,
   584  		consolidators.MatchOptions{MatchType: consolidators.MatchTags},
   585  		models.NewTagOptions(),
   586  		consolidators.LimitOptions{
   587  			Limit:             300,
   588  			RequireExhaustive: false,
   589  		},
   590  	)
   591  	result.Add(consolidators.MultiFetchResults{
   592  		SeriesIterators: iters,
   593  		Metadata:        block.NewResultMetadata(),
   594  		Attrs: storagemetadata.Attributes{
   595  			MetricsType: 0,
   596  			Retention:   30,
   597  			Resolution:  30,
   598  		},
   599  		Err: nil,
   600  	})
   601  	return result
   602  }