github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/storage/m3/cluster_resolver_test.go (about)

     1  // Copyright (c) 2019 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 m3
    22  
    23  import (
    24  	"sort"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/m3db/m3/src/dbnode/client"
    30  	"github.com/m3db/m3/src/metrics/policy"
    31  	"github.com/m3db/m3/src/query/storage"
    32  	"github.com/m3db/m3/src/query/storage/m3/consolidators"
    33  	"github.com/m3db/m3/src/query/storage/m3/storagemetadata"
    34  	xerrors "github.com/m3db/m3/src/x/errors"
    35  	"github.com/m3db/m3/src/x/ident"
    36  	xtime "github.com/m3db/m3/src/x/time"
    37  
    38  	"github.com/golang/mock/gomock"
    39  	"github.com/stretchr/testify/assert"
    40  	"github.com/stretchr/testify/require"
    41  )
    42  
    43  func TestFanoutAggregatedOptimizationDisabledGivesAllClustersAsPartial(t *testing.T) {
    44  	ctrl := gomock.NewController(t)
    45  	defer ctrl.Finish()
    46  
    47  	now := xtime.Now()
    48  	s, _ := setup(t, ctrl)
    49  	store, ok := s.(*m3storage)
    50  	assert.True(t, ok)
    51  	var r reusedAggregatedNamespaceSlices
    52  	opts := &storage.FanoutOptions{
    53  		FanoutAggregatedOptimized: storage.FanoutForceDisable,
    54  	}
    55  
    56  	clusters := store.clusters.ClusterNamespaces()
    57  	r = aggregatedNamespaces(clusters, r, nil, now, now, opts)
    58  	assert.Equal(t, 0, len(r.completeAggregated))
    59  	assert.Equal(t, 4, len(r.partialAggregated))
    60  }
    61  
    62  func TestFanoutUnaggregatedDisableReturnsAggregatedNamespaces(t *testing.T) {
    63  	ctrl := gomock.NewController(t)
    64  	defer ctrl.Finish()
    65  	s, _ := setup(t, ctrl)
    66  	store, ok := s.(*m3storage)
    67  	assert.True(t, ok)
    68  	opts := &storage.FanoutOptions{
    69  		FanoutUnaggregated: storage.FanoutForceDisable,
    70  	}
    71  
    72  	start := xtime.Now()
    73  	end := start.Add(time.Hour * 24 * -90)
    74  	_, clusters, err := resolveClusterNamespacesForQuery(start, start, end, store.clusters, opts,
    75  		nil, nil)
    76  	require.NoError(t, err)
    77  	require.Equal(t, 1, len(clusters))
    78  	assert.Equal(t, "metrics_aggregated_1m:30d", clusters[0].NamespaceID().String())
    79  }
    80  
    81  func TestFanoutUnaggregatedEnabledReturnsUnaggregatedNamespaces(t *testing.T) {
    82  	ctrl := gomock.NewController(t)
    83  	defer ctrl.Finish()
    84  	s, _ := setup(t, ctrl)
    85  	store, ok := s.(*m3storage)
    86  	assert.True(t, ok)
    87  	opts := &storage.FanoutOptions{
    88  		FanoutUnaggregated: storage.FanoutForceEnable,
    89  	}
    90  
    91  	start := xtime.Now()
    92  	end := start.Add(time.Hour * 24 * -90)
    93  	_, clusters, err := resolveClusterNamespacesForQuery(start,
    94  		start, end, store.clusters, opts, nil, nil)
    95  	require.NoError(t, err)
    96  	require.Equal(t, 1, len(clusters))
    97  	assert.Equal(t, "metrics_unaggregated", clusters[0].NamespaceID().String())
    98  }
    99  
   100  func TestGraphitePath(t *testing.T) {
   101  	ctrl := gomock.NewController(t)
   102  	defer ctrl.Finish()
   103  	s, _ := setup(t, ctrl)
   104  	store, ok := s.(*m3storage)
   105  	assert.True(t, ok)
   106  	opts := &storage.FanoutOptions{
   107  		FanoutUnaggregated:        storage.FanoutForceDisable,
   108  		FanoutAggregated:          storage.FanoutForceEnable,
   109  		FanoutAggregatedOptimized: storage.FanoutForceDisable,
   110  	}
   111  
   112  	start := xtime.Now()
   113  	end := start.Add(time.Second * -30)
   114  	_, clusters, err := resolveClusterNamespacesForQuery(start, start, end, store.clusters, opts,
   115  		nil, nil)
   116  	require.NoError(t, err)
   117  	require.Equal(t, 4, len(clusters))
   118  	expected := []string{
   119  		"metrics_aggregated_1m:30d", "metrics_aggregated_5m:90d",
   120  		"metrics_aggregated_partial_1m:180d", "metrics_aggregated_10m:365d",
   121  	}
   122  
   123  	for i, cluster := range clusters {
   124  		assert.Equal(t, expected[i], cluster.NamespaceID().String())
   125  	}
   126  }
   127  
   128  var (
   129  	genRetentionFiltered, genRetentionUnfiltered = time.Hour, time.Hour * 10
   130  	genResolution                                = time.Minute
   131  )
   132  
   133  func generateClusters(t *testing.T, ctrl *gomock.Controller) Clusters {
   134  	session := client.NewMockSession(ctrl)
   135  
   136  	clusters, err := NewClusters(UnaggregatedClusterNamespaceDefinition{
   137  		NamespaceID: ident.StringID("UNAGG"),
   138  		Retention:   genRetentionFiltered + time.Minute,
   139  		Session:     session,
   140  	}, AggregatedClusterNamespaceDefinition{
   141  		NamespaceID: ident.StringID("AGG_FILTERED"),
   142  		Retention:   genRetentionFiltered,
   143  		Resolution:  genResolution,
   144  		Downsample:  &ClusterNamespaceDownsampleOptions{All: false},
   145  		Session:     session,
   146  	}, AggregatedClusterNamespaceDefinition{
   147  		NamespaceID: ident.StringID("AGG_NO_FILTER"),
   148  		Retention:   genRetentionUnfiltered,
   149  		Resolution:  genResolution,
   150  		Downsample:  &ClusterNamespaceDownsampleOptions{All: false},
   151  		Session:     session,
   152  	}, AggregatedClusterNamespaceDefinition{
   153  		NamespaceID: ident.StringID("AGG_FILTERED_COMPLETE"),
   154  		Retention:   genRetentionFiltered,
   155  		Resolution:  genResolution + time.Second,
   156  		Downsample:  &ClusterNamespaceDownsampleOptions{All: true},
   157  		Session:     session,
   158  	}, AggregatedClusterNamespaceDefinition{
   159  		NamespaceID: ident.StringID("AGG_NO_FILTER_COMPLETE"),
   160  		Retention:   genRetentionUnfiltered,
   161  		Resolution:  genResolution + time.Second,
   162  		Downsample:  &ClusterNamespaceDownsampleOptions{All: true},
   163  		Session:     session,
   164  	})
   165  
   166  	require.NoError(t, err)
   167  	return clusters
   168  }
   169  
   170  // used to generate storage.RelatedQueries during a test
   171  type testTimeOffsets struct {
   172  	startOffset time.Duration
   173  	endOffset   time.Duration
   174  }
   175  
   176  var testCases = []struct {
   177  	name                     string
   178  	queryLength              time.Duration
   179  	opts                     *storage.FanoutOptions
   180  	restrict                 *storage.RestrictQueryOptions
   181  	expectedType             consolidators.QueryFanoutType
   182  	relatedQueryOffsets      []testTimeOffsets
   183  	expectedClusterNames     []string
   184  	expectedErr              error
   185  	expectedErrContains      string
   186  	expectedErrInvalidParams bool
   187  }{
   188  	{
   189  		name: "all disabled",
   190  		opts: &storage.FanoutOptions{
   191  			FanoutUnaggregated:        storage.FanoutForceDisable,
   192  			FanoutAggregated:          storage.FanoutForceDisable,
   193  			FanoutAggregatedOptimized: storage.FanoutForceDisable,
   194  		},
   195  		expectedType: consolidators.NamespaceInvalid,
   196  		expectedErr:  errUnaggregatedAndAggregatedDisabled,
   197  	},
   198  	{
   199  		name: "optimize enabled",
   200  		opts: &storage.FanoutOptions{
   201  			FanoutUnaggregated:        storage.FanoutForceDisable,
   202  			FanoutAggregated:          storage.FanoutForceDisable,
   203  			FanoutAggregatedOptimized: storage.FanoutForceEnable,
   204  		},
   205  		expectedType: consolidators.NamespaceInvalid,
   206  		expectedErr:  errUnaggregatedAndAggregatedDisabled,
   207  	},
   208  	{
   209  		name: "unagg enabled",
   210  		opts: &storage.FanoutOptions{
   211  			FanoutUnaggregated:        storage.FanoutForceEnable,
   212  			FanoutAggregated:          storage.FanoutForceDisable,
   213  			FanoutAggregatedOptimized: storage.FanoutForceDisable,
   214  		},
   215  		expectedType:         consolidators.NamespaceCoversPartialQueryRange,
   216  		expectedClusterNames: []string{"UNAGG"},
   217  	},
   218  	{
   219  		name:        "unagg enabled short range",
   220  		queryLength: time.Minute,
   221  		opts: &storage.FanoutOptions{
   222  			FanoutUnaggregated:        storage.FanoutForceEnable,
   223  			FanoutAggregated:          storage.FanoutForceDisable,
   224  			FanoutAggregatedOptimized: storage.FanoutForceDisable,
   225  		},
   226  		expectedType:         consolidators.NamespaceCoversAllQueryRange,
   227  		expectedClusterNames: []string{"UNAGG"},
   228  	},
   229  	{
   230  		name: "unagg optimized enabled",
   231  		opts: &storage.FanoutOptions{
   232  			FanoutUnaggregated:        storage.FanoutForceEnable,
   233  			FanoutAggregated:          storage.FanoutForceDisable,
   234  			FanoutAggregatedOptimized: storage.FanoutForceEnable,
   235  		},
   236  		expectedType:         consolidators.NamespaceCoversPartialQueryRange,
   237  		expectedClusterNames: []string{"UNAGG"},
   238  	},
   239  	{
   240  		name:        "unagg optimized enabled short range",
   241  		queryLength: time.Minute,
   242  		opts: &storage.FanoutOptions{
   243  			FanoutUnaggregated:        storage.FanoutForceEnable,
   244  			FanoutAggregated:          storage.FanoutForceDisable,
   245  			FanoutAggregatedOptimized: storage.FanoutForceEnable,
   246  		},
   247  		expectedType:         consolidators.NamespaceCoversAllQueryRange,
   248  		expectedClusterNames: []string{"UNAGG"},
   249  	},
   250  	{
   251  		name: "agg enabled",
   252  		opts: &storage.FanoutOptions{
   253  			FanoutUnaggregated:        storage.FanoutForceDisable,
   254  			FanoutAggregated:          storage.FanoutForceEnable,
   255  			FanoutAggregatedOptimized: storage.FanoutForceDisable,
   256  		},
   257  		expectedType: consolidators.NamespaceCoversPartialQueryRange,
   258  		expectedClusterNames: []string{
   259  			"AGG_FILTERED", "AGG_NO_FILTER",
   260  			"AGG_FILTERED_COMPLETE", "AGG_NO_FILTER_COMPLETE",
   261  		},
   262  	},
   263  	{
   264  		name:        "agg enabled short range",
   265  		queryLength: time.Minute,
   266  		opts: &storage.FanoutOptions{
   267  			FanoutUnaggregated:        storage.FanoutForceDisable,
   268  			FanoutAggregated:          storage.FanoutForceEnable,
   269  			FanoutAggregatedOptimized: storage.FanoutForceDisable,
   270  		},
   271  		expectedType: consolidators.NamespaceCoversAllQueryRange,
   272  		expectedClusterNames: []string{
   273  			"AGG_FILTERED", "AGG_NO_FILTER",
   274  			"AGG_FILTERED_COMPLETE", "AGG_NO_FILTER_COMPLETE",
   275  		},
   276  	},
   277  	{
   278  		name: "unagg and agg enabled",
   279  		opts: &storage.FanoutOptions{
   280  			FanoutUnaggregated:        storage.FanoutForceEnable,
   281  			FanoutAggregated:          storage.FanoutForceEnable,
   282  			FanoutAggregatedOptimized: storage.FanoutForceDisable,
   283  		},
   284  		expectedType: consolidators.NamespaceCoversPartialQueryRange,
   285  		expectedClusterNames: []string{
   286  			"UNAGG", "AGG_FILTERED", "AGG_NO_FILTER",
   287  			"AGG_FILTERED_COMPLETE", "AGG_NO_FILTER_COMPLETE",
   288  		},
   289  	},
   290  	{
   291  		name:        "unagg and agg enabled short range",
   292  		queryLength: time.Minute,
   293  		opts: &storage.FanoutOptions{
   294  			FanoutUnaggregated:        storage.FanoutForceEnable,
   295  			FanoutAggregated:          storage.FanoutForceEnable,
   296  			FanoutAggregatedOptimized: storage.FanoutForceDisable,
   297  		},
   298  		expectedType:         consolidators.NamespaceCoversAllQueryRange,
   299  		expectedClusterNames: []string{"UNAGG"},
   300  	},
   301  	{
   302  		name: "agg and optimization enabled",
   303  		opts: &storage.FanoutOptions{
   304  			FanoutUnaggregated:        storage.FanoutForceDisable,
   305  			FanoutAggregated:          storage.FanoutForceEnable,
   306  			FanoutAggregatedOptimized: storage.FanoutForceEnable,
   307  		},
   308  		expectedType:         consolidators.NamespaceCoversAllQueryRange,
   309  		expectedClusterNames: []string{"AGG_NO_FILTER", "AGG_NO_FILTER_COMPLETE"},
   310  	},
   311  	{
   312  		name:        "all enabled short range",
   313  		queryLength: time.Minute,
   314  		opts: &storage.FanoutOptions{
   315  			FanoutUnaggregated:        storage.FanoutForceEnable,
   316  			FanoutAggregated:          storage.FanoutForceEnable,
   317  			FanoutAggregatedOptimized: storage.FanoutForceEnable,
   318  		},
   319  		expectedType:         consolidators.NamespaceCoversAllQueryRange,
   320  		expectedClusterNames: []string{"UNAGG"},
   321  	},
   322  	{
   323  		name:        "all enabled long range",
   324  		queryLength: time.Hour * 1000,
   325  		opts: &storage.FanoutOptions{
   326  			FanoutUnaggregated:        storage.FanoutForceEnable,
   327  			FanoutAggregated:          storage.FanoutForceEnable,
   328  			FanoutAggregatedOptimized: storage.FanoutForceEnable,
   329  		},
   330  		expectedType:         consolidators.NamespaceCoversPartialQueryRange,
   331  		expectedClusterNames: []string{"AGG_NO_FILTER", "AGG_NO_FILTER_COMPLETE"},
   332  	},
   333  	{
   334  		name:        "restrict to unaggregated",
   335  		queryLength: time.Hour * 1000,
   336  		restrict: &storage.RestrictQueryOptions{
   337  			RestrictByType: &storage.RestrictByType{
   338  				MetricsType: storagemetadata.UnaggregatedMetricsType,
   339  			},
   340  		},
   341  		expectedType:         consolidators.NamespaceCoversPartialQueryRange,
   342  		expectedClusterNames: []string{"UNAGG"},
   343  	},
   344  	{
   345  		name:        "restrict to aggregate filtered",
   346  		queryLength: time.Hour * 1000,
   347  		restrict: &storage.RestrictQueryOptions{
   348  			RestrictByType: &storage.RestrictByType{
   349  				MetricsType: storagemetadata.AggregatedMetricsType,
   350  				StoragePolicy: policy.MustParseStoragePolicy(
   351  					genResolution.String() + ":" + genRetentionFiltered.String()),
   352  			},
   353  		},
   354  		expectedType:         consolidators.NamespaceCoversPartialQueryRange,
   355  		expectedClusterNames: []string{"AGG_FILTERED"},
   356  	},
   357  	{
   358  		name:        "restrict to aggregate unfiltered",
   359  		queryLength: time.Hour * 1000,
   360  		restrict: &storage.RestrictQueryOptions{
   361  			RestrictByType: &storage.RestrictByType{
   362  				MetricsType: storagemetadata.AggregatedMetricsType,
   363  				StoragePolicy: policy.MustParseStoragePolicy(
   364  					genResolution.String() + ":" + genRetentionUnfiltered.String()),
   365  			},
   366  		},
   367  		expectedType:         consolidators.NamespaceCoversPartialQueryRange,
   368  		expectedClusterNames: []string{"AGG_NO_FILTER"},
   369  	},
   370  	{
   371  		name:        "restrict with unknown metrics type",
   372  		queryLength: time.Hour * 1000,
   373  		restrict: &storage.RestrictQueryOptions{
   374  			RestrictByType: &storage.RestrictByType{
   375  				MetricsType: storagemetadata.UnknownMetricsType,
   376  			},
   377  		},
   378  		expectedErrContains:      "unrecognized metrics type:",
   379  		expectedErrInvalidParams: true,
   380  	},
   381  	{
   382  		name:        "restrict with unknown storage policy",
   383  		queryLength: time.Hour * 1000,
   384  		restrict: &storage.RestrictQueryOptions{
   385  			RestrictByType: &storage.RestrictByType{
   386  				MetricsType:   storagemetadata.AggregatedMetricsType,
   387  				StoragePolicy: policy.MustParseStoragePolicy("1s:100d"),
   388  			},
   389  		},
   390  		expectedErrContains:      "could not find namespace for storage policy:",
   391  		expectedErrInvalidParams: true,
   392  	},
   393  	{
   394  		name:        "restrict to multiple aggregate",
   395  		queryLength: time.Hour * 1000,
   396  		restrict: &storage.RestrictQueryOptions{
   397  			RestrictByTypes: []*storage.RestrictByType{
   398  				{
   399  					MetricsType: storagemetadata.AggregatedMetricsType,
   400  					StoragePolicy: policy.MustParseStoragePolicy(
   401  						genResolution.String() + ":" + genRetentionFiltered.String()),
   402  				},
   403  				{
   404  					MetricsType: storagemetadata.AggregatedMetricsType,
   405  					StoragePolicy: policy.MustParseStoragePolicy(
   406  						(genResolution + time.Second).String() + ":" + genRetentionUnfiltered.String()),
   407  				},
   408  			},
   409  		},
   410  		expectedType:         consolidators.NamespaceCoversPartialQueryRange,
   411  		expectedClusterNames: []string{"AGG_FILTERED", "AGG_NO_FILTER_COMPLETE"},
   412  	},
   413  	{
   414  		name:        "restrict to multiple aggregate all range",
   415  		queryLength: time.Minute,
   416  		opts: &storage.FanoutOptions{
   417  			FanoutUnaggregated:        storage.FanoutForceDisable,
   418  			FanoutAggregated:          storage.FanoutDefault,
   419  			FanoutAggregatedOptimized: storage.FanoutForceDisable,
   420  		},
   421  		restrict: &storage.RestrictQueryOptions{
   422  			RestrictByTypes: []*storage.RestrictByType{
   423  				{
   424  					MetricsType: storagemetadata.AggregatedMetricsType,
   425  					StoragePolicy: policy.MustParseStoragePolicy(
   426  						(genResolution + time.Second).String() + ":" + genRetentionFiltered.String()),
   427  				},
   428  				{
   429  					MetricsType: storagemetadata.AggregatedMetricsType,
   430  					StoragePolicy: policy.MustParseStoragePolicy(
   431  						(genResolution + time.Second).String() + ":" + genRetentionUnfiltered.String()),
   432  				},
   433  			},
   434  		},
   435  		expectedType:         consolidators.NamespaceCoversAllQueryRange,
   436  		expectedClusterNames: []string{"AGG_FILTERED_COMPLETE", "AGG_NO_FILTER_COMPLETE"},
   437  	},
   438  	{
   439  		name:        "all enabled short range w/ related queries",
   440  		queryLength: time.Minute,
   441  		opts: &storage.FanoutOptions{
   442  			FanoutUnaggregated:        storage.FanoutForceEnable,
   443  			FanoutAggregated:          storage.FanoutForceEnable,
   444  			FanoutAggregatedOptimized: storage.FanoutForceEnable,
   445  		},
   446  		relatedQueryOffsets:  []testTimeOffsets{{startOffset: time.Hour * 1000, endOffset: 0}},
   447  		expectedType:         consolidators.NamespaceCoversPartialQueryRange,
   448  		expectedClusterNames: []string{"AGG_NO_FILTER", "AGG_NO_FILTER_COMPLETE"},
   449  	},
   450  }
   451  
   452  func TestResolveClusterNamespacesForQueryWithOptions(t *testing.T) {
   453  	ctrl := gomock.NewController(t)
   454  	defer ctrl.Finish()
   455  	clusters := generateClusters(t, ctrl)
   456  
   457  	now := xtime.Now()
   458  	end := now
   459  	for _, tt := range testCases {
   460  		t.Run(tt.name, func(t *testing.T) {
   461  			start := now.Add(tt.queryLength * -1)
   462  			if tt.queryLength == 0 {
   463  				// default case
   464  				start = start.Add(time.Hour * -2)
   465  			}
   466  
   467  			relatedQueries := make([]storage.QueryTimespan, 0, len(tt.relatedQueryOffsets))
   468  			for _, offset := range tt.relatedQueryOffsets {
   469  				timespan := storage.QueryTimespan{
   470  					Start: now.Add(-offset.startOffset),
   471  					End:   now.Add(-offset.endOffset),
   472  				}
   473  				relatedQueries = append(relatedQueries, timespan)
   474  			}
   475  
   476  			fanoutType, clusters, err := resolveClusterNamespacesForQuery(now,
   477  				start, end, clusters, tt.opts, tt.restrict,
   478  				&storage.RelatedQueryOptions{Timespans: relatedQueries})
   479  			if tt.expectedErr != nil {
   480  				assert.Error(t, err)
   481  				assert.Equal(t, tt.expectedErr, err)
   482  				assert.Nil(t, clusters)
   483  				return
   484  			}
   485  
   486  			if substr := tt.expectedErrContains; substr != "" {
   487  				assert.Error(t, err)
   488  				assert.True(t, strings.Contains(err.Error(), substr))
   489  				assert.Nil(t, clusters)
   490  				invalidParams := xerrors.IsInvalidParams(err)
   491  				assert.Equal(t, tt.expectedErrInvalidParams, invalidParams)
   492  				return
   493  			}
   494  
   495  			require.NoError(t, err)
   496  			actualNames := make([]string, len(clusters))
   497  			for i, c := range clusters {
   498  				actualNames[i] = c.NamespaceID().String()
   499  			}
   500  
   501  			// NB: order does not matter.
   502  			sort.Strings(actualNames)
   503  			sort.Strings(tt.expectedClusterNames)
   504  			assert.Equal(t, tt.expectedClusterNames, actualNames)
   505  			assert.Equal(t, tt.expectedType, fanoutType)
   506  		})
   507  	}
   508  }
   509  
   510  func TestLongUnaggregatedRetention(t *testing.T) {
   511  	ctrl := gomock.NewController(t)
   512  	defer ctrl.Finish()
   513  	session := client.NewMockSession(ctrl)
   514  	retentionFiltered, retentionUnfiltered := time.Hour, time.Hour*10
   515  	resolution := time.Minute
   516  
   517  	clusters, err := NewClusters(UnaggregatedClusterNamespaceDefinition{
   518  		NamespaceID: ident.StringID("UNAGG"),
   519  		Retention:   retentionUnfiltered,
   520  		Session:     session,
   521  	}, AggregatedClusterNamespaceDefinition{
   522  		NamespaceID: ident.StringID("AGG_FILTERED"),
   523  		Retention:   retentionFiltered,
   524  		Resolution:  resolution,
   525  		Downsample:  &ClusterNamespaceDownsampleOptions{All: false},
   526  		Session:     session,
   527  	}, AggregatedClusterNamespaceDefinition{
   528  		NamespaceID: ident.StringID("AGG_NO_FILTER"),
   529  		Retention:   retentionUnfiltered + time.Minute,
   530  		Resolution:  resolution,
   531  		Downsample:  &ClusterNamespaceDownsampleOptions{All: false},
   532  		Session:     session,
   533  	}, AggregatedClusterNamespaceDefinition{
   534  		NamespaceID: ident.StringID("AGG_FILTERED_COMPLETE"),
   535  		Retention:   retentionFiltered,
   536  		Resolution:  resolution + time.Second,
   537  		Downsample:  &ClusterNamespaceDownsampleOptions{All: true},
   538  		Session:     session,
   539  	}, AggregatedClusterNamespaceDefinition{
   540  		NamespaceID: ident.StringID("AGG_NO_FILTER_COMPLETE"),
   541  		Retention:   retentionUnfiltered,
   542  		Resolution:  resolution + time.Second,
   543  		Downsample:  &ClusterNamespaceDownsampleOptions{All: true},
   544  		Session:     session,
   545  	})
   546  
   547  	require.NoError(t, err)
   548  	now := xtime.Now()
   549  	end := now
   550  	start := now.Add(time.Hour * -100)
   551  	opts := &storage.FanoutOptions{
   552  		FanoutUnaggregated:        storage.FanoutForceEnable,
   553  		FanoutAggregated:          storage.FanoutForceEnable,
   554  		FanoutAggregatedOptimized: storage.FanoutForceEnable,
   555  	}
   556  
   557  	fanoutType, ns, err := resolveClusterNamespacesForQuery(now, start, end, clusters,
   558  		opts, nil, nil)
   559  
   560  	require.NoError(t, err)
   561  	actualNames := make([]string, len(ns))
   562  	for i, c := range ns {
   563  		actualNames[i] = c.NamespaceID().String()
   564  	}
   565  
   566  	expected := []string{"UNAGG", "AGG_NO_FILTER"}
   567  	// NB: order does not matter.
   568  	sort.Strings(actualNames)
   569  	sort.Strings(expected)
   570  	assert.Equal(t, expected, actualNames)
   571  	assert.Equal(t, consolidators.NamespaceCoversPartialQueryRange, fanoutType)
   572  }
   573  
   574  func TestExampleCase(t *testing.T) {
   575  	ctrl := gomock.NewController(t)
   576  	defer ctrl.Finish()
   577  
   578  	session := client.NewMockSession(ctrl)
   579  	ns, err := NewClusters(
   580  		UnaggregatedClusterNamespaceDefinition{
   581  			NamespaceID: ident.StringID("metrics_10s_24h"),
   582  			Retention:   24 * time.Hour,
   583  			Session:     session,
   584  		}, AggregatedClusterNamespaceDefinition{
   585  			NamespaceID: ident.StringID("metrics_180s_360h"),
   586  			Retention:   360 * time.Hour,
   587  			Resolution:  120 * time.Second,
   588  			Downsample:  &ClusterNamespaceDownsampleOptions{All: false},
   589  			Session:     session,
   590  		}, AggregatedClusterNamespaceDefinition{
   591  			NamespaceID: ident.StringID("metrics_600s_17520h"),
   592  			Retention:   17520 * time.Hour,
   593  			Resolution:  600 * time.Second,
   594  			Downsample:  &ClusterNamespaceDownsampleOptions{All: false},
   595  			Session:     session,
   596  		},
   597  	)
   598  	require.NoError(t, err)
   599  
   600  	now := xtime.Now()
   601  	end := now
   602  
   603  	for i := 27; i < 17520; i++ {
   604  		start := now.Add(time.Hour * -1 * time.Duration(i))
   605  		fanoutType, clusters, err := resolveClusterNamespacesForQuery(now, start, end, ns,
   606  			&storage.FanoutOptions{}, nil, nil)
   607  
   608  		require.NoError(t, err)
   609  		actualNames := make([]string, len(clusters))
   610  		for i, c := range clusters {
   611  			actualNames[i] = c.NamespaceID().String()
   612  		}
   613  
   614  		// NB: order does not matter.
   615  		sort.Strings(actualNames)
   616  		assert.Equal(t, []string{
   617  			"metrics_10s_24h",
   618  			"metrics_180s_360h", "metrics_600s_17520h",
   619  		}, actualNames)
   620  		assert.Equal(t, consolidators.NamespaceCoversPartialQueryRange, fanoutType)
   621  	}
   622  }
   623  
   624  func TestDeduplicatePartialAggregateNamespaces(t *testing.T) {
   625  	ctrl := gomock.NewController(t)
   626  	defer ctrl.Finish()
   627  
   628  	session := client.NewMockSession(ctrl)
   629  	ns, err := NewClusters(UnaggregatedClusterNamespaceDefinition{
   630  		NamespaceID: ident.StringID("default"),
   631  		Retention:   24 * time.Hour,
   632  		Session:     session,
   633  	}, AggregatedClusterNamespaceDefinition{
   634  		NamespaceID: ident.StringID("aggregated_block_6h"),
   635  		Retention:   360 * time.Hour,
   636  		Resolution:  10 * time.Minute,
   637  		Downsample:  &ClusterNamespaceDownsampleOptions{All: false},
   638  		Session:     session,
   639  	}, AggregatedClusterNamespaceDefinition{
   640  		NamespaceID: ident.StringID("aggregated_block_6h"),
   641  		Retention:   360 * time.Hour,
   642  		Resolution:  1 * time.Hour,
   643  		Downsample:  &ClusterNamespaceDownsampleOptions{All: true},
   644  		Session:     session,
   645  	})
   646  	require.NoError(t, err)
   647  
   648  	now := xtime.Now()
   649  	end := now
   650  
   651  	start := now.Add(-48 * time.Hour)
   652  	fanoutType, clusters, err := resolveClusterNamespacesForQuery(now, start, end, ns,
   653  		&storage.FanoutOptions{}, nil, nil)
   654  	require.NoError(t, err)
   655  
   656  	actualNames := make([]string, len(clusters))
   657  	for i, c := range clusters {
   658  		actualNames[i] = c.NamespaceID().String()
   659  	}
   660  
   661  	// NB: order does not matter.
   662  	sort.Strings(actualNames)
   663  	assert.Equal(t, []string{"aggregated_block_6h"}, actualNames)
   664  	assert.Equal(t, consolidators.NamespaceCoversAllQueryRange, fanoutType)
   665  }
   666  
   667  func TestResolveNamespaceWithDataLatency(t *testing.T) {
   668  	ctrl := gomock.NewController(t)
   669  	defer ctrl.Finish()
   670  
   671  	dataLatency := 10 * time.Hour
   672  
   673  	session := client.NewMockSession(ctrl)
   674  	ns, err := NewClusters(
   675  		UnaggregatedClusterNamespaceDefinition{
   676  			NamespaceID: ident.StringID("default"),
   677  			Retention:   24 * time.Hour,
   678  			Session:     session,
   679  		},
   680  		AggregatedClusterNamespaceDefinition{
   681  			NamespaceID: ident.StringID("aggregated_30d"),
   682  			Retention:   30 * 24 * time.Hour,
   683  			Resolution:  5 * time.Minute,
   684  			Downsample:  &ClusterNamespaceDownsampleOptions{All: true},
   685  			Session:     session,
   686  		},
   687  		AggregatedClusterNamespaceDefinition{
   688  			NamespaceID: ident.StringID("aggregated_60d"),
   689  			Retention:   60 * 24 * time.Hour,
   690  			Resolution:  10 * time.Minute,
   691  			Downsample:  &ClusterNamespaceDownsampleOptions{All: true},
   692  			DataLatency: dataLatency,
   693  			Session:     session,
   694  		},
   695  	)
   696  	require.NoError(t, err)
   697  
   698  	var (
   699  		now   = xtime.Now()
   700  		start = now.Add(-40 * 24 * time.Hour)
   701  		end   = now.Add(-3 * time.Hour)
   702  	)
   703  
   704  	fanoutType, clusters, err := resolveClusterNamespacesForQuery(now, start, end, ns,
   705  		&storage.FanoutOptions{}, nil, nil)
   706  	require.NoError(t, err)
   707  
   708  	actualNamespaces := make(map[string]narrowing)
   709  	for _, c := range clusters {
   710  		actualNamespaces[c.NamespaceID().String()] = c.narrowing
   711  	}
   712  
   713  	stitchAt := now.Add(-dataLatency).Truncate(10 * time.Minute)
   714  	expectedNamespaces := map[string]narrowing{
   715  		"default":        {start: stitchAt},
   716  		"aggregated_60d": {end: stitchAt},
   717  	}
   718  
   719  	assert.Equal(t, expectedNamespaces, actualNamespaces)
   720  	assert.Equal(t, consolidators.NamespaceCoversAllQueryRange, fanoutType)
   721  }