github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/storage/m3/cluster_resolver.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  	"fmt"
    25  	"sort"
    26  
    27  	"github.com/m3db/m3/src/query/storage"
    28  	"github.com/m3db/m3/src/query/storage/m3/consolidators"
    29  	"github.com/m3db/m3/src/query/storage/m3/storagemetadata"
    30  	xerrors "github.com/m3db/m3/src/x/errors"
    31  	xtime "github.com/m3db/m3/src/x/time"
    32  )
    33  
    34  type unaggregatedNamespaceType uint8
    35  
    36  const (
    37  	partiallySatisfiesRange unaggregatedNamespaceType = iota
    38  	fullySatisfiesRange
    39  	disabled
    40  )
    41  
    42  type unaggregatedNamespaceDetails struct {
    43  	satisfies        unaggregatedNamespaceType
    44  	clusterNamespace ClusterNamespace
    45  }
    46  
    47  type resolvedNamespaces []resolvedNamespace
    48  
    49  type resolvedNamespace struct {
    50  	ClusterNamespace
    51  	narrowing narrowing
    52  }
    53  
    54  func resolved(ns ClusterNamespace) resolvedNamespace {
    55  	return resolvedNamespace{ClusterNamespace: ns}
    56  }
    57  
    58  // resolveUnaggregatedNamespaceForQuery determines if the unaggregated namespace
    59  // should be used, and if so, determines if it fully satisfies the query range.
    60  func resolveUnaggregatedNamespaceForQuery(
    61  	now, start xtime.UnixNano,
    62  	unaggregated ClusterNamespace,
    63  	opts *storage.FanoutOptions,
    64  ) unaggregatedNamespaceDetails {
    65  	if opts.FanoutUnaggregated == storage.FanoutForceDisable {
    66  		return unaggregatedNamespaceDetails{satisfies: disabled}
    67  	}
    68  
    69  	var (
    70  		retention         = unaggregated.Options().Attributes().Retention
    71  		unaggregatedStart = now.Add(-1 * retention)
    72  	)
    73  
    74  	satisfies := fullySatisfiesRange
    75  	if unaggregatedStart.After(start) {
    76  		satisfies = partiallySatisfiesRange
    77  	}
    78  
    79  	return unaggregatedNamespaceDetails{
    80  		clusterNamespace: unaggregated,
    81  		satisfies:        satisfies,
    82  	}
    83  }
    84  
    85  // resolveClusterNamespacesForQuery returns the namespaces that need to be
    86  // fanned out to depending on the query time and the namespaces configured.
    87  func resolveClusterNamespacesForQuery(
    88  	now,
    89  	start,
    90  	end xtime.UnixNano,
    91  	clusters Clusters,
    92  	opts *storage.FanoutOptions,
    93  	restrict *storage.RestrictQueryOptions,
    94  	relatedQueryOpts *storage.RelatedQueryOptions,
    95  ) (consolidators.QueryFanoutType, resolvedNamespaces, error) {
    96  	// Calculate a new start time if related query opts are present.
    97  	// NB: We do not calculate a new end time because it does not factor
    98  	// into namespace selection.
    99  	namespaceSelectionStart := start
   100  	if relatedQueryOpts != nil {
   101  		for _, timeRange := range relatedQueryOpts.Timespans {
   102  			if timeRange.Start < namespaceSelectionStart {
   103  				namespaceSelectionStart = timeRange.Start
   104  			}
   105  		}
   106  	}
   107  
   108  	// 1. First resolve the logical plan.
   109  	fanout, namespaces, err := resolveClusterNamespacesForQueryLogicalPlan(now,
   110  		namespaceSelectionStart, end, clusters, opts, restrict)
   111  	if err != nil {
   112  		return fanout, namespaces, err
   113  	}
   114  
   115  	// 2. Create physical plan.
   116  	// Now de-duplicate any namespaces that might be fetched twice due to
   117  	// the fact some of the same namespaces are reused once for unaggregated
   118  	// and another for aggregated rollups (which don't collide with timeseries).
   119  	filtered := namespaces[:0]
   120  	for _, ns := range namespaces {
   121  		keep := true
   122  		// Small enough that we can do n^2 here instead of creating a map,
   123  		// usually less than 4 namespaces resolved.
   124  		for _, existing := range filtered {
   125  			if ns.NamespaceID().Equal(existing.NamespaceID()) {
   126  				keep = false
   127  				break
   128  			}
   129  		}
   130  		if !keep {
   131  			continue
   132  		}
   133  		filtered = append(filtered, ns)
   134  	}
   135  
   136  	return fanout, filtered, nil
   137  }
   138  
   139  // resolveClusterNamespacesForQueryLogicalPlan resolves the logical plan
   140  // for namespaces to query.
   141  // nolint: unparam
   142  func resolveClusterNamespacesForQueryLogicalPlan(
   143  	now, start, end xtime.UnixNano,
   144  	clusters Clusters,
   145  	opts *storage.FanoutOptions,
   146  	restrict *storage.RestrictQueryOptions,
   147  ) (consolidators.QueryFanoutType, resolvedNamespaces, error) {
   148  	if typeRestrict := restrict.GetRestrictByType(); typeRestrict != nil {
   149  		// If a specific restriction is set, then attempt to satisfy.
   150  		return resolveClusterNamespacesForQueryWithTypeRestrictQueryOptions(now,
   151  			start, clusters, *typeRestrict)
   152  	}
   153  
   154  	if typesRestrict := restrict.GetRestrictByTypes(); typesRestrict != nil {
   155  		// If a specific restriction is set, then attempt to satisfy.
   156  		return resolveClusterNamespacesForQueryWithTypesRestrictQueryOptions(now,
   157  			start, clusters, typesRestrict)
   158  	}
   159  
   160  	// First check if the unaggregated cluster can fully satisfy the query range.
   161  	// If so, return it and shortcircuit, as unaggregated will necessarily have
   162  	// every metric.
   163  	ns, initialized := clusters.UnaggregatedClusterNamespace()
   164  	if !initialized {
   165  		return consolidators.NamespaceInvalid, nil, errUnaggregatedNamespaceUninitialized
   166  	}
   167  
   168  	unaggregated := resolveUnaggregatedNamespaceForQuery(now, start, ns, opts)
   169  	if unaggregated.satisfies == fullySatisfiesRange {
   170  		return consolidators.NamespaceCoversAllQueryRange,
   171  			resolvedNamespaces{resolved(unaggregated.clusterNamespace)},
   172  			nil
   173  	}
   174  
   175  	if opts.FanoutAggregated == storage.FanoutForceDisable {
   176  		if unaggregated.satisfies == partiallySatisfiesRange {
   177  			return consolidators.NamespaceCoversPartialQueryRange,
   178  				resolvedNamespaces{resolved(unaggregated.clusterNamespace)}, nil
   179  		}
   180  
   181  		return consolidators.NamespaceInvalid, nil, errUnaggregatedAndAggregatedDisabled
   182  	}
   183  
   184  	// The filter function will drop namespaces which do not cover the entire
   185  	// query range from contention.
   186  	//
   187  	// NB: if fanout aggregation is forced on, the filter instead forces clusters
   188  	// that do not cover the range to be set as partially aggregated.
   189  	coversRangeFilter := newCoversRangeFilter(coversRangeFilterOptions{
   190  		now:        now,
   191  		queryStart: start,
   192  	})
   193  
   194  	// Filter aggregated namespaces by filter function and options.
   195  	var r reusedAggregatedNamespaceSlices
   196  	r = aggregatedNamespaces(clusters.ClusterNamespaces(), r, coversRangeFilter, now, end, opts)
   197  
   198  	// If any of the aggregated clusters have a complete set of metrics, use
   199  	// those that have the smallest resolutions, supplemented by lower resolution
   200  	// partially aggregated metrics.
   201  	if len(r.completeAggregated) > 0 {
   202  		sort.Stable(resolvedNamespacesByResolutionAsc(r.completeAggregated))
   203  		// Take most granular complete aggregated namespace.
   204  		result := r.completeAggregated[:1]
   205  		completedAttrs := result[0].Options().Attributes()
   206  		// Also include any finer grain partially aggregated namespaces that
   207  		// may contain a matching metric.
   208  		for _, n := range r.partialAggregated {
   209  			if n.Options().Attributes().Resolution < completedAttrs.Resolution {
   210  				// More granular resolution.
   211  				result = append(result, n)
   212  			}
   213  		}
   214  
   215  		if unaggregatedNarrowed, ok := mustStitchWithUnaggregated(result[0].narrowing, unaggregated); ok {
   216  			result = append(result, unaggregatedNarrowed)
   217  		}
   218  
   219  		return consolidators.NamespaceCoversAllQueryRange, result, nil
   220  	}
   221  
   222  	// No complete aggregated namespaces can definitely fulfill the query,
   223  	// so take the longest retention completed aggregated namespace to return
   224  	// as much data as possible, along with any partially aggregated namespaces
   225  	// that have either same retention and lower resolution or longer retention
   226  	// than the complete aggregated namespace.
   227  	r = aggregatedNamespaces(clusters.ClusterNamespaces(), r, nil, now, end, opts)
   228  	if len(r.completeAggregated) == 0 {
   229  		// Absolutely no complete aggregated namespaces, need to fanout to all
   230  		// partial aggregated namespaces as well as the unaggregated cluster
   231  		// as we have no idea which has the longest retention.
   232  		result := r.partialAggregated
   233  		// If unaggregated namespace can partially satisfy this range, add it as a
   234  		// fanout contender.
   235  		if unaggregated.satisfies == partiallySatisfiesRange {
   236  			result = append(result, resolved(unaggregated.clusterNamespace))
   237  		}
   238  
   239  		// If any namespace currently in contention does not cover the entire query
   240  		// range, set query fanout type to namespaceCoversPartialQueryRange.
   241  		for _, n := range result {
   242  			if !coversRangeFilter(n) {
   243  				return consolidators.NamespaceCoversPartialQueryRange, result, nil
   244  			}
   245  		}
   246  
   247  		// Otherwise, all namespaces cover the query range.
   248  		return consolidators.NamespaceCoversAllQueryRange, result, nil
   249  	}
   250  
   251  	// Return the longest retention aggregated namespace and
   252  	// any potentially more granular or longer retention partial
   253  	// aggregated namespaces.
   254  	sort.Stable(sort.Reverse(resolvedNamespacesByRetentionAsc(r.completeAggregated)))
   255  
   256  	// Take longest retention complete aggregated namespace or the unaggregated
   257  	// cluster if that is longer than the longest aggregated namespace.
   258  	result := r.completeAggregated[:1]
   259  	completedAttrs := result[0].Options().Attributes()
   260  	if unaggregated.satisfies == partiallySatisfiesRange {
   261  		unaggregatedAttrs := unaggregated.clusterNamespace.Options().Attributes()
   262  		if completedAttrs.Retention <= unaggregatedAttrs.Retention {
   263  			// If the longest aggregated cluster for some reason has lower retention
   264  			// than the unaggregated cluster then we prefer the unaggregated cluster
   265  			// as it has a complete data set and is always the most granular.
   266  			result[0] = resolved(unaggregated.clusterNamespace)
   267  			completedAttrs = unaggregated.clusterNamespace.Options().Attributes()
   268  		}
   269  	}
   270  
   271  	if unaggregatedNarrowed, ok := mustStitchWithUnaggregated(result[0].narrowing, unaggregated); ok {
   272  		result = append(result, unaggregatedNarrowed)
   273  	}
   274  
   275  	// Take any partially aggregated namespaces with longer retention or
   276  	// same retention with more granular resolution that may contain
   277  	// a matching metric.
   278  	for _, n := range r.partialAggregated {
   279  		attrs := n.Options().Attributes()
   280  		if attrs.Retention > completedAttrs.Retention {
   281  			// Higher retention.
   282  			result = append(result, n)
   283  		} else if attrs.Retention == completedAttrs.Retention &&
   284  			attrs.Resolution < completedAttrs.Resolution {
   285  			// Same retention but more granular resolution.
   286  			result = append(result, n)
   287  		}
   288  	}
   289  
   290  	return consolidators.NamespaceCoversPartialQueryRange, result, nil
   291  }
   292  
   293  type reusedAggregatedNamespaceSlices struct {
   294  	completeAggregated resolvedNamespaces
   295  	partialAggregated  resolvedNamespaces
   296  }
   297  
   298  func (slices reusedAggregatedNamespaceSlices) reset(
   299  	size int,
   300  ) reusedAggregatedNamespaceSlices {
   301  	// Initialize arrays if yet uninitialized.
   302  	if slices.completeAggregated == nil {
   303  		slices.completeAggregated = make(resolvedNamespaces, 0, size)
   304  	} else {
   305  		slices.completeAggregated = slices.completeAggregated[:0]
   306  	}
   307  
   308  	if slices.partialAggregated == nil {
   309  		slices.partialAggregated = make(resolvedNamespaces, 0, size)
   310  	} else {
   311  		slices.partialAggregated = slices.partialAggregated[:0]
   312  	}
   313  
   314  	return slices
   315  }
   316  
   317  // aggregatedNamespaces filters out clusters that do not meet the filter
   318  // condition, and organizes remaining clusters in two lists if possible.
   319  //
   320  // NB: If fanout aggregation is disabled, no clusters will be returned as either
   321  // partial or complete candidates. If fanout aggregation is forced to enabled
   322  // then no filter is applied, and all namespaces are considered viable. In this
   323  // case, the filter is used to determine if returned namespaces have the
   324  // complete set of metrics.
   325  //
   326  // NB: If fanout optimization is enabled, add any aggregated namespaces that
   327  // have a complete set of metrics to the completeAggregated slice list. If this
   328  // optimization is disabled, or if none of the aggregated namespaces are
   329  // guaranteed to have a complete set of all metrics, they are added to the
   330  // partialAggregated list.
   331  func aggregatedNamespaces(
   332  	all ClusterNamespaces,
   333  	slices reusedAggregatedNamespaceSlices,
   334  	filter func(ClusterNamespace) bool,
   335  	now, end xtime.UnixNano,
   336  	opts *storage.FanoutOptions,
   337  ) reusedAggregatedNamespaceSlices {
   338  	// Reset reused slices.
   339  	slices = slices.reset(len(all))
   340  
   341  	// Otherwise the default and force enable is to fanout and treat
   342  	// the aggregated namespaces differently (depending on whether they
   343  	// have all the data).
   344  	for _, namespace := range all {
   345  		nsOpts := namespace.Options()
   346  		if nsOpts.Attributes().MetricsType != storagemetadata.AggregatedMetricsType {
   347  			// Not an aggregated cluster.
   348  			continue
   349  		}
   350  
   351  		if filter != nil && !filter(namespace) {
   352  			// Fails to satisfy filter.
   353  			continue
   354  		}
   355  
   356  		resolvedNs := resolved(namespace)
   357  
   358  		var (
   359  			dataLatency        = nsOpts.DataLatency()
   360  			resolution         = nsOpts.Attributes().Resolution
   361  			dataAvailableUntil = now.Add(-dataLatency).Truncate(resolution)
   362  		)
   363  		if dataLatency > 0 && end.After(dataAvailableUntil) {
   364  			resolvedNs.narrowing.end = dataAvailableUntil
   365  		}
   366  
   367  		// If not optimizing fanout to aggregated namespaces, set all aggregated
   368  		// namespaces satisfying the filter as partially aggregated, as all metrics
   369  		// do not necessarily appear in all namespaces, depending on configuration.
   370  		if opts.FanoutAggregatedOptimized == storage.FanoutForceDisable {
   371  			slices.partialAggregated = append(slices.partialAggregated, resolvedNs)
   372  			continue
   373  		}
   374  
   375  		// Otherwise, check downsample options for the namespace and determine if
   376  		// this namespace is set as containing all metrics.
   377  		downsampleOpts, err := nsOpts.DownsampleOptions()
   378  		if err != nil {
   379  			continue
   380  		}
   381  
   382  		if downsampleOpts.All {
   383  			// This namespace has a complete set of metrics. Ensure that it passes
   384  			// the filter if it was a forced addition, otherwise it may be too short
   385  			// to cover the entire range and should be considered a partial result.
   386  			slices.completeAggregated = append(slices.completeAggregated, resolvedNs)
   387  			continue
   388  		}
   389  
   390  		// This namespace does not necessarily have a complete set of metrics.
   391  		slices.partialAggregated = append(slices.partialAggregated, resolvedNs)
   392  	}
   393  
   394  	return slices
   395  }
   396  
   397  // resolveClusterNamespacesForQueryWithTypeRestrictQueryOptions returns the cluster
   398  // namespace referred to by the restrict fetch options or an error if it
   399  // cannot be found.
   400  func resolveClusterNamespacesForQueryWithTypeRestrictQueryOptions(
   401  	now, start xtime.UnixNano,
   402  	clusters Clusters,
   403  	restrict storage.RestrictByType,
   404  ) (consolidators.QueryFanoutType, resolvedNamespaces, error) {
   405  	coversRangeFilter := newCoversRangeFilter(coversRangeFilterOptions{
   406  		now:        now,
   407  		queryStart: start,
   408  	})
   409  
   410  	result := func(
   411  		namespace ClusterNamespace,
   412  		err error,
   413  	) (consolidators.QueryFanoutType, resolvedNamespaces, error) {
   414  		if err != nil {
   415  			return 0, nil, err
   416  		}
   417  
   418  		if coversRangeFilter(namespace) {
   419  			return consolidators.NamespaceCoversAllQueryRange,
   420  				resolvedNamespaces{resolved(namespace)}, nil
   421  		}
   422  
   423  		return consolidators.NamespaceCoversPartialQueryRange,
   424  			resolvedNamespaces{resolved(namespace)}, nil
   425  	}
   426  
   427  	switch restrict.MetricsType {
   428  	case storagemetadata.UnaggregatedMetricsType:
   429  		ns, ok := clusters.UnaggregatedClusterNamespace()
   430  		if !ok {
   431  			return result(nil,
   432  				fmt.Errorf("could not find unaggregated namespace for storage policy: %v",
   433  					restrict.StoragePolicy.String()))
   434  		}
   435  		return result(ns, nil)
   436  	case storagemetadata.AggregatedMetricsType:
   437  		ns, ok := clusters.AggregatedClusterNamespace(RetentionResolution{
   438  			Retention:  restrict.StoragePolicy.Retention().Duration(),
   439  			Resolution: restrict.StoragePolicy.Resolution().Window,
   440  		})
   441  		if !ok {
   442  			err := xerrors.NewInvalidParamsError(
   443  				fmt.Errorf("could not find namespace for storage policy: %v",
   444  					restrict.StoragePolicy.String()))
   445  			return result(nil, err)
   446  		}
   447  
   448  		return result(ns, nil)
   449  	default:
   450  		err := xerrors.NewInvalidParamsError(
   451  			fmt.Errorf("unrecognized metrics type: %v", restrict.MetricsType))
   452  		return result(nil, err)
   453  	}
   454  }
   455  
   456  // resolveClusterNamespacesForQueryWithTypesRestrictQueryOptions returns the cluster
   457  // namespace referred to by the array of restrict fetch options or an error if it
   458  // cannot be found.
   459  func resolveClusterNamespacesForQueryWithTypesRestrictQueryOptions(
   460  	now, start xtime.UnixNano,
   461  	clusters Clusters,
   462  	restricts []*storage.RestrictByType,
   463  ) (consolidators.QueryFanoutType, resolvedNamespaces, error) {
   464  	var (
   465  		namespaces resolvedNamespaces
   466  		fanoutType consolidators.QueryFanoutType
   467  	)
   468  	for _, restrict := range restricts {
   469  		t, ns, err := resolveClusterNamespacesForQueryWithTypeRestrictQueryOptions(now, start, clusters, *restrict)
   470  		if err != nil {
   471  			return consolidators.NamespaceInvalid, nil, err
   472  		}
   473  		namespaces = append(namespaces, ns...)
   474  		if t == consolidators.NamespaceCoversPartialQueryRange ||
   475  			fanoutType == consolidators.NamespaceCoversPartialQueryRange {
   476  			fanoutType = consolidators.NamespaceCoversPartialQueryRange
   477  		} else {
   478  			fanoutType = consolidators.NamespaceCoversAllQueryRange
   479  		}
   480  	}
   481  	return fanoutType, namespaces, nil
   482  }
   483  
   484  func mustStitchWithUnaggregated(
   485  	narrowing narrowing,
   486  	unaggregated unaggregatedNamespaceDetails,
   487  ) (resolvedNamespace, bool) {
   488  	if !narrowing.end.IsZero() {
   489  		// completeAggregated namespace will not have the most recent data available, will
   490  		// have to query unaggregated namespace for it and then stitch the responses together.
   491  		unaggregatedNarrowed := resolved(unaggregated.clusterNamespace)
   492  		unaggregatedNarrowed.narrowing.start = narrowing.end
   493  
   494  		return unaggregatedNarrowed, true
   495  	}
   496  
   497  	return resolvedNamespace{}, false
   498  }
   499  
   500  type coversRangeFilterOptions struct {
   501  	now        xtime.UnixNano
   502  	queryStart xtime.UnixNano
   503  }
   504  
   505  func newCoversRangeFilter(opts coversRangeFilterOptions) func(namespace ClusterNamespace) bool {
   506  	return func(namespace ClusterNamespace) bool {
   507  		// Include only if can fulfill the entire time range of the query
   508  		clusterStart := opts.now.Add(-1 * namespace.Options().Attributes().Retention)
   509  		return !clusterStart.After(opts.queryStart)
   510  	}
   511  }
   512  
   513  type resolvedNamespacesByResolutionAsc resolvedNamespaces
   514  
   515  func (a resolvedNamespacesByResolutionAsc) Len() int      { return len(a) }
   516  func (a resolvedNamespacesByResolutionAsc) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
   517  func (a resolvedNamespacesByResolutionAsc) Less(i, j int) bool {
   518  	return a[i].Options().Attributes().Resolution < a[j].Options().Attributes().Resolution
   519  }
   520  
   521  type resolvedNamespacesByRetentionAsc resolvedNamespaces
   522  
   523  func (a resolvedNamespacesByRetentionAsc) Len() int      { return len(a) }
   524  func (a resolvedNamespacesByRetentionAsc) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
   525  func (a resolvedNamespacesByRetentionAsc) Less(i, j int) bool {
   526  	return a[i].Options().Attributes().Retention < a[j].Options().Attributes().Retention
   527  }