github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/metrics/rules/active_ruleset.go (about)

     1  // Copyright (c) 2020 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 rules
    22  
    23  import (
    24  	"bytes"
    25  	"fmt"
    26  	"sort"
    27  
    28  	"github.com/m3db/m3/src/metrics/aggregation"
    29  	"github.com/m3db/m3/src/metrics/filters"
    30  	"github.com/m3db/m3/src/metrics/metadata"
    31  	"github.com/m3db/m3/src/metrics/metric"
    32  	metricid "github.com/m3db/m3/src/metrics/metric/id"
    33  	mpipeline "github.com/m3db/m3/src/metrics/pipeline"
    34  	"github.com/m3db/m3/src/metrics/pipeline/applied"
    35  	"github.com/m3db/m3/src/metrics/rules/view"
    36  	"github.com/m3db/m3/src/query/models"
    37  	xerrors "github.com/m3db/m3/src/x/errors"
    38  )
    39  
    40  type activeRuleSet struct {
    41  	version         int
    42  	mappingRules    []*mappingRule
    43  	rollupRules     []*rollupRule
    44  	cutoverTimesAsc []int64
    45  	tagsFilterOpts  filters.TagsFilterOptions
    46  	newRollupIDFn   metricid.NewIDFn
    47  	isRollupIDFn    metricid.MatchIDFn
    48  }
    49  
    50  func newActiveRuleSet(
    51  	version int,
    52  	mappingRules []*mappingRule,
    53  	rollupRules []*rollupRule,
    54  	tagsFilterOpts filters.TagsFilterOptions,
    55  	newRollupIDFn metricid.NewIDFn,
    56  	isRollupIDFn metricid.MatchIDFn,
    57  ) *activeRuleSet {
    58  	uniqueCutoverTimes := make(map[int64]struct{})
    59  	for _, mappingRule := range mappingRules {
    60  		for _, snapshot := range mappingRule.snapshots {
    61  			uniqueCutoverTimes[snapshot.cutoverNanos] = struct{}{}
    62  		}
    63  	}
    64  	for _, rollupRule := range rollupRules {
    65  		for _, snapshot := range rollupRule.snapshots {
    66  			uniqueCutoverTimes[snapshot.cutoverNanos] = struct{}{}
    67  		}
    68  	}
    69  
    70  	cutoverTimesAsc := make([]int64, 0, len(uniqueCutoverTimes))
    71  	for t := range uniqueCutoverTimes {
    72  		cutoverTimesAsc = append(cutoverTimesAsc, t)
    73  	}
    74  	sort.Sort(int64Asc(cutoverTimesAsc))
    75  
    76  	return &activeRuleSet{
    77  		version:         version,
    78  		mappingRules:    mappingRules,
    79  		rollupRules:     rollupRules,
    80  		cutoverTimesAsc: cutoverTimesAsc,
    81  		tagsFilterOpts:  tagsFilterOpts,
    82  		newRollupIDFn:   newRollupIDFn,
    83  		isRollupIDFn:    isRollupIDFn,
    84  	}
    85  }
    86  
    87  // The forward matching logic goes like this:
    88  //
    89  // Imagine you have the list of rules in the ruleset lined up vertically. Each rule may have one
    90  // or more snapshots, each of which represents a change to that rule (e.g., filter change, policy
    91  // change, etc.). These snapshots are naturally non-overlapping in time since only one snapshot
    92  // can be active at a given point in time. As a result, if we use the x axis to represent time,
    93  // then for each rule, a snapshot is active for some amount of time. IOW, if you pick a time and
    94  // draw a vertical line across the set of rules, the snapshots of different ruels that intersect
    95  // with the vertical line are the active rule snapshots for the ruleset.
    96  //
    97  // Now you have a list of times you need to perform rule matching at. Each matching time
    98  // corresponds to a cutover time of a rule in the ruleset, because that's when matching the metric
    99  // ID against this rule may lead to a different metadata including different storage policies and
   100  // new rollup IDs to be generated or existing rollup IDs to stop being generated. The final match
   101  // result is a collection of such metadata sorted by time in ascending order.
   102  //
   103  // NB(xichen): can further consolidate consecutive staged metadata to deduplicate.
   104  func (as *activeRuleSet) ForwardMatch(
   105  	id metricid.ID,
   106  	fromNanos, toNanos int64,
   107  	opts MatchOptions,
   108  ) (MatchResult, error) {
   109  	currMatchRes, err := as.forwardMatchAt(id.Bytes(), fromNanos, opts)
   110  	if err != nil {
   111  		return MatchResult{}, err
   112  	}
   113  	var (
   114  		forExistingID    = metadata.StagedMetadatas{currMatchRes.forExistingID}
   115  		forNewRollupIDs  = currMatchRes.forNewRollupIDs
   116  		nextIdx          = as.nextCutoverIdx(fromNanos)
   117  		nextCutoverNanos = as.cutoverNanosAt(nextIdx)
   118  		keepOriginal     = currMatchRes.keepOriginal
   119  	)
   120  
   121  	for nextIdx < len(as.cutoverTimesAsc) && nextCutoverNanos < toNanos {
   122  		nextMatchRes, err := as.forwardMatchAt(id.Bytes(), nextCutoverNanos, opts)
   123  		if err != nil {
   124  			return MatchResult{}, err
   125  		}
   126  		forExistingID = mergeResultsForExistingID(forExistingID, nextMatchRes.forExistingID, nextCutoverNanos)
   127  		forNewRollupIDs = mergeResultsForNewRollupIDs(forNewRollupIDs, nextMatchRes.forNewRollupIDs, nextCutoverNanos)
   128  		nextIdx++
   129  		nextCutoverNanos = as.cutoverNanosAt(nextIdx)
   130  		keepOriginal = nextMatchRes.keepOriginal
   131  	}
   132  
   133  	// The result expires when the beginning of the match time range reaches the first cutover time
   134  	// after `fromNanos`, or the end of the match time range reaches the first cutover time after
   135  	// `toNanos` among all active rules because the metric may then be matched against a different
   136  	// set of rules.
   137  	return NewMatchResult(
   138  		as.version,
   139  		nextCutoverNanos,
   140  		forExistingID,
   141  		forNewRollupIDs,
   142  		keepOriginal,
   143  	), nil
   144  }
   145  
   146  func (as *activeRuleSet) ReverseMatch(
   147  	id metricid.ID,
   148  	fromNanos, toNanos int64,
   149  	mt metric.Type,
   150  	at aggregation.Type,
   151  	isMultiAggregationTypesAllowed bool,
   152  	aggTypesOpts aggregation.TypesOptions,
   153  ) (MatchResult, error) {
   154  	var (
   155  		nextIdx          = as.nextCutoverIdx(fromNanos)
   156  		nextCutoverNanos = as.cutoverNanosAt(nextIdx)
   157  		forExistingID    metadata.StagedMetadatas
   158  		isRollupID       bool
   159  		keepOriginal     bool
   160  	)
   161  
   162  	// Determine whether the ID is a rollup metric ID.
   163  	name, tags, err := as.tagsFilterOpts.NameAndTagsFn(id.Bytes())
   164  	if err == nil {
   165  		isRollupID = as.isRollupIDFn(name, tags)
   166  	}
   167  
   168  	currResult, found, err := as.reverseMappingsFor(
   169  		id.Bytes(),
   170  		name,
   171  		tags,
   172  		isRollupID,
   173  		fromNanos,
   174  		mt,
   175  		at,
   176  		isMultiAggregationTypesAllowed,
   177  		aggTypesOpts,
   178  	)
   179  	if err != nil {
   180  		return MatchResult{}, err
   181  	}
   182  	if found {
   183  		forExistingID = mergeResultsForExistingID(forExistingID, currResult.metadata, fromNanos)
   184  		if currResult.keepOriginal {
   185  			keepOriginal = true
   186  		}
   187  	}
   188  
   189  	for nextIdx < len(as.cutoverTimesAsc) && nextCutoverNanos < toNanos {
   190  		nextResult, found, err := as.reverseMappingsFor(
   191  			id.Bytes(),
   192  			name,
   193  			tags,
   194  			isRollupID,
   195  			nextCutoverNanos,
   196  			mt,
   197  			at,
   198  			isMultiAggregationTypesAllowed,
   199  			aggTypesOpts,
   200  		)
   201  		if err != nil {
   202  			return MatchResult{}, err
   203  		}
   204  		if found {
   205  			forExistingID = mergeResultsForExistingID(
   206  				forExistingID,
   207  				nextResult.metadata,
   208  				nextCutoverNanos,
   209  			)
   210  			if nextResult.keepOriginal {
   211  				keepOriginal = true
   212  			}
   213  		}
   214  
   215  		nextIdx++
   216  		nextCutoverNanos = as.cutoverNanosAt(nextIdx)
   217  	}
   218  	return NewMatchResult(as.version, nextCutoverNanos, forExistingID, nil, keepOriginal), nil
   219  }
   220  
   221  // NB(xichen): can further consolidate pipelines with the same aggregation ID
   222  // and same applied pipeline but different storage policies to reduce amount of
   223  // data that needed to be stored in memory and sent across the wire.
   224  func (as *activeRuleSet) forwardMatchAt(
   225  	id []byte,
   226  	timeNanos int64,
   227  	matchOpts MatchOptions,
   228  ) (forwardMatchResult, error) {
   229  	mappingResults, err := as.mappingsForNonRollupID(id, timeNanos, matchOpts)
   230  	if err != nil {
   231  		return forwardMatchResult{}, err
   232  	}
   233  	rollupResults, err := as.rollupResultsFor(id, timeNanos, matchOpts)
   234  	if err != nil {
   235  		return forwardMatchResult{}, err
   236  	}
   237  	forExistingID := mappingResults.forExistingID.
   238  		merge(rollupResults.forExistingID).
   239  		unique().
   240  		toStagedMetadata()
   241  	forNewRollupIDs := make([]IDWithMetadatas, 0, len(rollupResults.forNewRollupIDs))
   242  	for _, idWithMatchResult := range rollupResults.forNewRollupIDs {
   243  		stagedMetadata := idWithMatchResult.matchResults.unique().toStagedMetadata()
   244  		newIDWithMetadatas := IDWithMetadatas{
   245  			ID:        idWithMatchResult.id,
   246  			Metadatas: metadata.StagedMetadatas{stagedMetadata},
   247  		}
   248  		forNewRollupIDs = append(forNewRollupIDs, newIDWithMetadatas)
   249  	}
   250  	sort.Sort(IDWithMetadatasByIDAsc(forNewRollupIDs))
   251  	return forwardMatchResult{
   252  		forExistingID:   forExistingID,
   253  		forNewRollupIDs: forNewRollupIDs,
   254  		keepOriginal:    rollupResults.keepOriginal,
   255  	}, nil
   256  }
   257  
   258  func (as *activeRuleSet) mappingsForNonRollupID(
   259  	id []byte,
   260  	timeNanos int64,
   261  	matchOpts MatchOptions,
   262  ) (mappingResults, error) {
   263  	var (
   264  		cutoverNanos int64
   265  		pipelines    []metadata.PipelineMetadata
   266  	)
   267  	for _, mappingRule := range as.mappingRules {
   268  		snapshot := mappingRule.activeSnapshot(timeNanos)
   269  		if snapshot == nil {
   270  			continue
   271  		}
   272  		matches, err := snapshot.filter.Matches(id, filters.TagMatchOptions{
   273  			SortedTagIteratorFn: matchOpts.SortedTagIteratorFn,
   274  			NameAndTagsFn:       matchOpts.NameAndTagsFn,
   275  		})
   276  		if err != nil {
   277  			return mappingResults{}, err
   278  		}
   279  		if !matches {
   280  			continue
   281  		}
   282  		// Make sure the cutover time tracks the latest cutover time among all matching
   283  		// mapping rules to represent the correct time of rule change.
   284  		if cutoverNanos < snapshot.cutoverNanos {
   285  			cutoverNanos = snapshot.cutoverNanos
   286  		}
   287  		// If the mapping rule snapshot is a tombstoned snapshot, its cutover time is
   288  		// recorded to indicate a rule change, but its policies are no longer in effect.
   289  		if snapshot.tombstoned {
   290  			continue
   291  		}
   292  		pipeline := metadata.PipelineMetadata{
   293  			AggregationID:   snapshot.aggregationID,
   294  			StoragePolicies: snapshot.storagePolicies.Clone(),
   295  			DropPolicy:      snapshot.dropPolicy,
   296  			Tags:            snapshot.tags,
   297  			GraphitePrefix:  snapshot.graphitePrefix,
   298  		}
   299  		pipelines = append(pipelines, pipeline)
   300  	}
   301  
   302  	// NB: The pipeline list should never be empty as the resulting pipelines are
   303  	// used to determine how the *existing* ID is aggregated and retained. If there
   304  	// are no rule match, the default pipeline list is used.
   305  	if len(pipelines) == 0 {
   306  		pipelines = metadata.DefaultPipelineMetadatas.Clone()
   307  	}
   308  	return mappingResults{
   309  		forExistingID: ruleMatchResults{cutoverNanos: cutoverNanos, pipelines: pipelines},
   310  	}, nil
   311  }
   312  
   313  func (as *activeRuleSet) LatestRollupRules(_ []byte, timeNanos int64) ([]view.RollupRule, error) {
   314  	out := []view.RollupRule{}
   315  	// Return the list of cloned rollup rule views that were active (and are still
   316  	// active) as of timeNanos.
   317  	for _, rollupRule := range as.rollupRules {
   318  		rule := rollupRule.activeRule(timeNanos)
   319  		// Skip missing or empty rules.
   320  		// tombstoned() returns true if the length of rule.snapshots is zero.
   321  		if rule == nil || rule.tombstoned() {
   322  			continue
   323  		}
   324  
   325  		view, err := rule.rollupRuleView(len(rule.snapshots) - 1)
   326  		if err != nil {
   327  			return nil, err
   328  		}
   329  		out = append(out, view)
   330  	}
   331  	return out, nil
   332  }
   333  
   334  func (as *activeRuleSet) rollupResultsFor(id []byte, timeNanos int64, matchOpts MatchOptions) (rollupResults, error) {
   335  	var (
   336  		cutoverNanos  int64
   337  		rollupTargets []rollupTarget
   338  		keepOriginal  bool
   339  		tags          [][]models.Tag
   340  	)
   341  
   342  	for _, rollupRule := range as.rollupRules {
   343  		snapshot := rollupRule.activeSnapshot(timeNanos)
   344  		if snapshot == nil {
   345  			continue
   346  		}
   347  		match, err := snapshot.filter.Matches(id, filters.TagMatchOptions{
   348  			NameAndTagsFn:       matchOpts.NameAndTagsFn,
   349  			SortedTagIteratorFn: matchOpts.SortedTagIteratorFn,
   350  		})
   351  		if err != nil {
   352  			return rollupResults{}, err
   353  		}
   354  		if !match {
   355  			continue
   356  		}
   357  
   358  		// Make sure the cutover time tracks the latest cutover time among all matching
   359  		// rollup rules to represent the correct time of rule change.
   360  		if cutoverNanos < snapshot.cutoverNanos {
   361  			cutoverNanos = snapshot.cutoverNanos
   362  		}
   363  
   364  		if snapshot.keepOriginal {
   365  			keepOriginal = true
   366  		}
   367  
   368  		// If the rollup rule snapshot is a tombstoned snapshot, its cutover time is
   369  		// recorded to indicate a rule change, but its rollup targets are no longer in effect.
   370  		if snapshot.tombstoned {
   371  			continue
   372  		}
   373  
   374  		for _, target := range snapshot.targets {
   375  			rollupTargets = append(rollupTargets, target.clone())
   376  			tags = append(tags, snapshot.tags)
   377  		}
   378  	}
   379  	// NB: could log the matching error here if needed.
   380  	res, _ := as.toRollupResults(id, cutoverNanos, rollupTargets, keepOriginal, tags, matchOpts)
   381  	return res, nil
   382  }
   383  
   384  // toRollupMatchResult applies the rollup operation in each rollup pipelines contained
   385  // in the rollup targets against the matching ID to determine the resulting new rollup
   386  // ID. It additionally distinguishes rollup pipelines whose first operation is a rollup
   387  // operation from those that aren't since the former pipelines are applied against the
   388  // original metric ID and the latter are applied against new rollup IDs due to the
   389  // application of the rollup operation.
   390  // nolint: unparam
   391  func (as *activeRuleSet) toRollupResults(
   392  	id []byte,
   393  	cutoverNanos int64,
   394  	targets []rollupTarget,
   395  	keepOriginal bool,
   396  	tags [][]models.Tag,
   397  	matchOpts MatchOptions,
   398  ) (rollupResults, error) {
   399  	if len(targets) == 0 {
   400  		return rollupResults{}, nil
   401  	}
   402  
   403  	// If we cannot extract tags from the id, this is likely an invalid
   404  	// metric and we bail early.
   405  	_, sortedTagPairBytes, err := matchOpts.NameAndTagsFn(id)
   406  	if err != nil {
   407  		return rollupResults{}, err
   408  	}
   409  
   410  	var (
   411  		multiErr           = xerrors.NewMultiError()
   412  		pipelines          = make([]metadata.PipelineMetadata, 0, len(targets))
   413  		newRollupIDResults = make([]idWithMatchResults, 0, len(targets))
   414  		tagPairs           []metricid.TagPair
   415  	)
   416  
   417  	for idx, target := range targets {
   418  		pipeline := target.Pipeline
   419  		// A rollup target should always have a non-empty pipeline but
   420  		// just being defensive here.
   421  		if pipeline.IsEmpty() {
   422  			err = fmt.Errorf("target %v has empty pipeline", target)
   423  			multiErr = multiErr.Add(err)
   424  			continue
   425  		}
   426  		var (
   427  			aggregationID aggregation.ID
   428  			rollupID      []byte
   429  			numSteps      = pipeline.Len()
   430  			firstOp       = pipeline.At(0)
   431  			toApply       mpipeline.Pipeline
   432  		)
   433  		switch firstOp.Type {
   434  		case mpipeline.AggregationOpType:
   435  			aggregationID, err = aggregation.CompressTypes(firstOp.Aggregation.Type)
   436  			if err != nil {
   437  				err = fmt.Errorf("target %v operation 0 aggregation type compression error: %v", target, err)
   438  				multiErr = multiErr.Add(err)
   439  				continue
   440  			}
   441  			toApply = pipeline.SubPipeline(1, numSteps)
   442  		case mpipeline.TransformationOpType:
   443  			aggregationID = aggregation.DefaultID
   444  			toApply = pipeline
   445  		case mpipeline.RollupOpType:
   446  			tagPairs = tagPairs[:0]
   447  			var matched bool
   448  			rollupID, matched, err = as.matchRollupTarget(
   449  				sortedTagPairBytes,
   450  				firstOp.Rollup,
   451  				tagPairs,
   452  				tags[idx],
   453  				matchRollupTargetOptions{generateRollupID: true},
   454  				matchOpts)
   455  			if err != nil {
   456  				multiErr = multiErr.Add(err)
   457  				continue
   458  			}
   459  			if !matched {
   460  				// The incoming metric ID did not match the rollup target.
   461  				continue
   462  			}
   463  			aggregationID = firstOp.Rollup.AggregationID
   464  			toApply = pipeline.SubPipeline(1, numSteps)
   465  		default:
   466  			err = fmt.Errorf("target %v operation 0 has unknown type: %v", target, firstOp.Type)
   467  			multiErr = multiErr.Add(err)
   468  			continue
   469  		}
   470  		tagPairs = tagPairs[:0]
   471  		applied, err := as.applyIDToPipeline(sortedTagPairBytes, toApply, tagPairs, tags[idx], matchOpts)
   472  		if err != nil {
   473  			err = fmt.Errorf("failed to apply id %s to pipeline %v: %v", id, toApply, err)
   474  			multiErr = multiErr.Add(err)
   475  			continue
   476  		}
   477  		newPipeline := metadata.PipelineMetadata{
   478  			AggregationID:   aggregationID,
   479  			StoragePolicies: target.StoragePolicies,
   480  			Pipeline:        applied,
   481  			ResendEnabled:   target.ResendEnabled,
   482  		}
   483  		if rollupID == nil {
   484  			// The applied pipeline applies to the incoming ID.
   485  			pipelines = append(pipelines, newPipeline)
   486  		} else {
   487  			if len(tags[idx]) > 0 {
   488  				newPipeline.Tags = tags[idx]
   489  			}
   490  			// The applied pipeline applies to a new rollup ID.
   491  			matchResults := ruleMatchResults{
   492  				cutoverNanos: cutoverNanos,
   493  				pipelines:    []metadata.PipelineMetadata{newPipeline},
   494  			}
   495  			newRollupIDResult := idWithMatchResults{id: rollupID, matchResults: matchResults}
   496  			newRollupIDResults = append(newRollupIDResults, newRollupIDResult)
   497  		}
   498  	}
   499  
   500  	return rollupResults{
   501  		forExistingID:   ruleMatchResults{cutoverNanos: cutoverNanos, pipelines: pipelines},
   502  		forNewRollupIDs: newRollupIDResults,
   503  		keepOriginal:    keepOriginal,
   504  	}, multiErr.FinalError()
   505  }
   506  
   507  // matchRollupTarget matches an incoming metric ID against a rollup target,
   508  // returns the new rollup ID if the metric ID contains the full list of rollup
   509  // tags, and nil otherwise.
   510  func (as *activeRuleSet) matchRollupTarget(
   511  	sortedTagPairBytes []byte,
   512  	rollupOp mpipeline.RollupOp,
   513  	tagPairs []metricid.TagPair, // buffer for reuse to generate rollup ID across calls
   514  	tags []models.Tag,
   515  	targetOpts matchRollupTargetOptions,
   516  	matchOpts MatchOptions,
   517  ) ([]byte, bool, error) {
   518  	if rollupOp.Type == mpipeline.ExcludeByRollupType && !targetOpts.generateRollupID {
   519  		// Exclude by tag always matches, if not generating rollup ID
   520  		// then immediately return.
   521  		return nil, true, nil
   522  	}
   523  
   524  	var (
   525  		rollupTags    = rollupOp.Tags
   526  		sortedTagIter = matchOpts.SortedTagIteratorFn(sortedTagPairBytes)
   527  		matchTagIdx   = 0
   528  		nameTagName   = as.tagsFilterOpts.NameTagKey
   529  		nameTagValue  []byte
   530  	)
   531  
   532  	switch rollupOp.Type {
   533  	case mpipeline.GroupByRollupType:
   534  		// Iterate through each tag, looking to match it with corresponding filter tags on the rule
   535  		//
   536  		// For include rules, every rule has to have a corresponding match. This means we return
   537  		// early whenever there's a missing match and increment matchRuleIdx whenever there is a match.
   538  		for hasMoreTags := sortedTagIter.Next(); hasMoreTags; hasMoreTags = sortedTagIter.Next() {
   539  			tagName, tagVal := sortedTagIter.Current()
   540  			// nolint:gosimple
   541  			isNameTag := bytes.Compare(tagName, nameTagName) == 0
   542  			if isNameTag {
   543  				nameTagValue = tagVal
   544  			}
   545  
   546  			// If we've matched all tags, no need to process.
   547  			// We don't break out of the for loop, because we may still need to find the name tag.
   548  			if matchTagIdx >= len(rollupTags) {
   549  				continue
   550  			}
   551  
   552  			res := bytes.Compare(tagName, rollupTags[matchTagIdx])
   553  			if res == 0 {
   554  				// Include grouped by tag.
   555  				if targetOpts.generateRollupID {
   556  					tagPairs = append(tagPairs, metricid.TagPair{Name: tagName, Value: tagVal})
   557  				}
   558  				matchTagIdx++
   559  				continue
   560  			}
   561  
   562  			// If one of the target tags is not found in the ID, this is considered  a non-match so return immediately.
   563  			if res > 0 {
   564  				return nil, false, nil
   565  			}
   566  		}
   567  	case mpipeline.ExcludeByRollupType:
   568  		// Iterate through each tag, looking to match it with corresponding filter tags on the rule.
   569  		//
   570  		// For exclude rules, this means merging with the tag rule list and incrementing the
   571  		// matchTagIdx whenever the current tag rule is lexigraphically greater than the rule tag,
   572  		// since we need to be careful in the case where there is no matching input tag for some rule.
   573  		for hasMoreTags := sortedTagIter.Next(); hasMoreTags; {
   574  			tagName, tagVal := sortedTagIter.Current()
   575  			// nolint:gosimple
   576  			isNameTag := bytes.Compare(tagName, nameTagName) == 0
   577  			if isNameTag {
   578  				nameTagValue = tagVal
   579  
   580  				// Don't copy name tag since we'll add that using the new rollup ID fn.
   581  				hasMoreTags = sortedTagIter.Next()
   582  				continue
   583  			}
   584  
   585  			if matchTagIdx >= len(rollupTags) {
   586  				// Have matched all the tags to exclude, just blindly copy.
   587  				if targetOpts.generateRollupID {
   588  					tagPairs = append(tagPairs, metricid.TagPair{Name: tagName, Value: tagVal})
   589  				}
   590  				hasMoreTags = sortedTagIter.Next()
   591  				continue
   592  			}
   593  
   594  			res := bytes.Compare(tagName, rollupTags[matchTagIdx])
   595  			if res > 0 {
   596  				// Current tag is greater than the current exclude rule,
   597  				// so we know the current exclude rule has no match and
   598  				// we should move on to the next one.
   599  				matchTagIdx++
   600  				continue
   601  			}
   602  
   603  			if res != 0 {
   604  				// Only include tags that don't match the exclude tag
   605  				if targetOpts.generateRollupID {
   606  					tagPairs = append(tagPairs, metricid.TagPair{Name: tagName, Value: tagVal})
   607  				}
   608  			}
   609  
   610  			hasMoreTags = sortedTagIter.Next()
   611  		}
   612  	}
   613  
   614  	if sortedTagIter.Err() != nil {
   615  		return nil, false, sortedTagIter.Err()
   616  	}
   617  
   618  	if !targetOpts.generateRollupID {
   619  		return nil, true, nil
   620  	}
   621  
   622  	for _, tag := range tags {
   623  		tagPairs = append(tagPairs, metricid.TagPair{
   624  			Name:  tag.Name,
   625  			Value: tag.Value,
   626  		})
   627  	}
   628  
   629  	newName := rollupOp.NewName(nameTagValue)
   630  	return as.newRollupIDFn(newName, tagPairs), true, nil
   631  }
   632  
   633  func (as *activeRuleSet) applyIDToPipeline(
   634  	sortedTagPairBytes []byte,
   635  	pipeline mpipeline.Pipeline,
   636  	tagPairs []metricid.TagPair, // buffer for reuse across calls
   637  	tags []models.Tag,
   638  	matchOpts MatchOptions,
   639  ) (applied.Pipeline, error) {
   640  	operations := make([]applied.OpUnion, 0, pipeline.Len())
   641  	for i := 0; i < pipeline.Len(); i++ {
   642  		pipelineOp := pipeline.At(i)
   643  		var opUnion applied.OpUnion
   644  		switch pipelineOp.Type {
   645  		case mpipeline.TransformationOpType:
   646  			opUnion = applied.OpUnion{
   647  				Type:           mpipeline.TransformationOpType,
   648  				Transformation: pipelineOp.Transformation,
   649  			}
   650  		case mpipeline.RollupOpType:
   651  			rollupOp := pipelineOp.Rollup
   652  			var matched bool
   653  			rollupID, matched, err := as.matchRollupTarget(
   654  				sortedTagPairBytes,
   655  				rollupOp,
   656  				tagPairs,
   657  				tags,
   658  				matchRollupTargetOptions{generateRollupID: true},
   659  				matchOpts)
   660  			if err != nil {
   661  				return applied.Pipeline{}, err
   662  			}
   663  			if !matched {
   664  				err := fmt.Errorf("existing tag pairs %s do not contain all rollup tags %s", sortedTagPairBytes, rollupOp.Tags)
   665  				return applied.Pipeline{}, err
   666  			}
   667  			opUnion = applied.OpUnion{
   668  				Type:   mpipeline.RollupOpType,
   669  				Rollup: applied.RollupOp{ID: rollupID, AggregationID: rollupOp.AggregationID},
   670  			}
   671  		default:
   672  			return applied.Pipeline{}, fmt.Errorf("unexpected pipeline op type: %v", pipelineOp.Type)
   673  		}
   674  		operations = append(operations, opUnion)
   675  	}
   676  	return applied.NewPipeline(operations), nil
   677  }
   678  
   679  func (as *activeRuleSet) reverseMappingsFor(
   680  	id, name, tags []byte,
   681  	isRollupID bool,
   682  	timeNanos int64,
   683  	mt metric.Type,
   684  	at aggregation.Type,
   685  	isMultiAggregationTypesAllowed bool,
   686  	aggTypesOpts aggregation.TypesOptions,
   687  ) (reverseMatchResult, bool, error) {
   688  	if !isRollupID {
   689  		return as.reverseMappingsForNonRollupID(id, timeNanos, mt, at, aggTypesOpts)
   690  	}
   691  	return as.reverseMappingsForRollupID(name, tags, timeNanos, mt, at, isMultiAggregationTypesAllowed, aggTypesOpts)
   692  }
   693  
   694  type reverseMatchResult struct {
   695  	metadata     metadata.StagedMetadata
   696  	keepOriginal bool
   697  }
   698  
   699  // reverseMappingsForNonRollupID returns the staged metadata for the given non-rollup ID at
   700  // the given time, and true if a non-empty list of pipelines are found, and false otherwise.
   701  func (as *activeRuleSet) reverseMappingsForNonRollupID(
   702  	id []byte,
   703  	timeNanos int64,
   704  	mt metric.Type,
   705  	at aggregation.Type,
   706  	aggTypesOpts aggregation.TypesOptions,
   707  ) (reverseMatchResult, bool, error) {
   708  	mapping, err := as.mappingsForNonRollupID(id, timeNanos, MatchOptions{
   709  		NameAndTagsFn:       as.tagsFilterOpts.NameAndTagsFn,
   710  		SortedTagIteratorFn: as.tagsFilterOpts.SortedTagIteratorFn,
   711  	})
   712  	if err != nil {
   713  		return reverseMatchResult{}, false, err
   714  	}
   715  	mappingRes := mapping.forExistingID
   716  	// Always filter pipelines with aggregation types because for non rollup IDs, it is possible
   717  	// that none of the rules would match based on the aggregation types, in which case we fall
   718  	// back to the default staged metadata.
   719  	filteredPipelines := filteredPipelinesWithAggregationType(mappingRes.pipelines, mt, at, aggTypesOpts)
   720  	if len(filteredPipelines) == 0 {
   721  		return reverseMatchResult{
   722  			metadata: metadata.DefaultStagedMetadata,
   723  		}, false, nil
   724  	}
   725  
   726  	return reverseMatchResult{
   727  		metadata: metadata.StagedMetadata{
   728  			CutoverNanos: mappingRes.cutoverNanos,
   729  			Tombstoned:   false,
   730  			Metadata:     metadata.Metadata{Pipelines: filteredPipelines},
   731  		},
   732  	}, true, nil
   733  }
   734  
   735  // NB(xichen): in order to determine the applicable policies for a rollup metric, we need to
   736  // match the id against rollup rules to determine which rollup rules are applicable, under the
   737  // assumption that no two rollup targets in the same namespace may have the same rollup metric
   738  // name and the list of rollup tags. Otherwise, a rollup metric could potentially match more
   739  // than one rollup rule with different policies even though only one of the matched rules was
   740  // used to produce the given rollup metric id due to its tag filters, thereby causing the wrong
   741  // staged policies to be returned. This also implies at any given time, at most one rollup target
   742  // may match the given rollup id.
   743  // Since we may have rollup pipelines with different aggregation types defined for a roll up rule,
   744  // and each aggregation type would generate a new id. So when doing reverse mapping, not only do
   745  // we need to match the roll up tags, we also need to check the aggregation type against
   746  // each rollup pipeline to see if the aggregation type was actually contained in the pipeline.
   747  func (as *activeRuleSet) reverseMappingsForRollupID(
   748  	name, sortedTagPairBytes []byte,
   749  	timeNanos int64,
   750  	mt metric.Type,
   751  	at aggregation.Type,
   752  	isMultiAggregationTypesAllowed bool,
   753  	aggTypesOpts aggregation.TypesOptions,
   754  ) (reverseMatchResult, bool, error) {
   755  	for _, rollupRule := range as.rollupRules {
   756  		snapshot := rollupRule.activeSnapshot(timeNanos)
   757  		if snapshot == nil || snapshot.tombstoned {
   758  			continue
   759  		}
   760  
   761  		for _, target := range snapshot.targets {
   762  			for i := 0; i < target.Pipeline.Len(); i++ {
   763  				pipelineOp := target.Pipeline.At(i)
   764  				if pipelineOp.Type != mpipeline.RollupOpType {
   765  					continue
   766  				}
   767  				rollupOp := pipelineOp.Rollup
   768  				if !bytes.Equal(rollupOp.NewName(name), name) {
   769  					continue
   770  				}
   771  				_, matched, err := as.matchRollupTarget(
   772  					sortedTagPairBytes,
   773  					rollupOp,
   774  					nil,
   775  					nil,
   776  					matchRollupTargetOptions{generateRollupID: false},
   777  					MatchOptions{
   778  						NameAndTagsFn:       as.tagsFilterOpts.NameAndTagsFn,
   779  						SortedTagIteratorFn: as.tagsFilterOpts.SortedTagIteratorFn,
   780  					},
   781  				)
   782  				if err != nil {
   783  					return reverseMatchResult{}, false, err
   784  				}
   785  				if !matched {
   786  					continue
   787  				}
   788  				// NB: the list of pipeline steps is not important and thus not computed and returned.
   789  				pipeline := metadata.PipelineMetadata{
   790  					AggregationID:   rollupOp.AggregationID,
   791  					StoragePolicies: target.StoragePolicies.Clone(),
   792  				}
   793  				// Only further filter the pipelines with aggregation types if the given metric type
   794  				// supports multiple aggregation types. This is because if a metric type only supports
   795  				// a single aggregation type, this is the only pipline that could possibly produce this
   796  				// rollup metric and as such is chosen. The aggregation type passed in is not used because
   797  				// it maybe not be accurate because it may not be possible to infer the actual aggregation
   798  				// type only from the metric ID.
   799  				filteredPipelines := []metadata.PipelineMetadata{pipeline}
   800  				if isMultiAggregationTypesAllowed {
   801  					filteredPipelines = filteredPipelinesWithAggregationType(filteredPipelines, mt, at, aggTypesOpts)
   802  				}
   803  				if len(filteredPipelines) == 0 {
   804  					return reverseMatchResult{
   805  						metadata: metadata.DefaultStagedMetadata,
   806  					}, false, nil
   807  				}
   808  
   809  				return reverseMatchResult{
   810  					metadata: metadata.StagedMetadata{
   811  						CutoverNanos: snapshot.cutoverNanos,
   812  						Tombstoned:   false,
   813  						Metadata:     metadata.Metadata{Pipelines: filteredPipelines},
   814  					},
   815  					keepOriginal: snapshot.keepOriginal,
   816  				}, true, nil
   817  			}
   818  		}
   819  	}
   820  	return reverseMatchResult{
   821  		metadata: metadata.DefaultStagedMetadata,
   822  	}, false, nil
   823  }
   824  
   825  // nextCutoverIdx returns the next snapshot index whose cutover time is after t.
   826  // NB(xichen): not using sort.Search to avoid a lambda capture.
   827  func (as *activeRuleSet) nextCutoverIdx(t int64) int {
   828  	i, j := 0, len(as.cutoverTimesAsc)
   829  	for i < j {
   830  		h := i + (j-i)/2
   831  		if as.cutoverTimesAsc[h] <= t {
   832  			i = h + 1
   833  		} else {
   834  			j = h
   835  		}
   836  	}
   837  	return i
   838  }
   839  
   840  // cutoverNanosAt returns the cutover time at given index.
   841  func (as *activeRuleSet) cutoverNanosAt(idx int) int64 {
   842  	if idx < len(as.cutoverTimesAsc) {
   843  		return as.cutoverTimesAsc[idx]
   844  	}
   845  	return timeNanosMax
   846  }
   847  
   848  // filterByAggregationType takes a list of pipelines as input and returns those
   849  // containing the given aggregation type.
   850  func filteredPipelinesWithAggregationType(
   851  	pipelines []metadata.PipelineMetadata,
   852  	mt metric.Type,
   853  	at aggregation.Type,
   854  	opts aggregation.TypesOptions,
   855  ) []metadata.PipelineMetadata {
   856  	var cur int
   857  	for i := 0; i < len(pipelines); i++ {
   858  		var containsAggType bool
   859  		if aggID := pipelines[i].AggregationID; aggID.IsDefault() {
   860  			containsAggType = opts.IsContainedInDefaultAggregationTypes(at, mt)
   861  		} else {
   862  			containsAggType = aggID.Contains(at)
   863  		}
   864  		if !containsAggType {
   865  			continue
   866  		}
   867  		if cur != i {
   868  			pipelines[cur] = pipelines[i]
   869  		}
   870  		cur++
   871  	}
   872  	return pipelines[:cur]
   873  }
   874  
   875  // mergeResultsForExistingID merges the next staged metadata into the current list of staged
   876  // metadatas while ensuring the cutover times of the staged metadatas are non-decreasing. This
   877  // is needed because the cutover times of staged metadata results produced by mapping rule matching
   878  // may not always be in ascending order. For example, if at time T0 a metric matches against a
   879  // mapping rule, and the filter of such rule changed at T1 such that the metric no longer matches
   880  // the rule, this would indicate the staged metadata at T0 would have a cutover time of T0,
   881  // whereas the staged metadata at T1 would have a cutover time of 0 (due to no rule match),
   882  // in which case we need to set the cutover time of the staged metadata at T1 to T1 to ensure
   883  // the mononicity of cutover times.
   884  func mergeResultsForExistingID(
   885  	currMetadatas metadata.StagedMetadatas,
   886  	nextMetadata metadata.StagedMetadata,
   887  	nextCutoverNanos int64,
   888  ) metadata.StagedMetadatas {
   889  	if len(currMetadatas) == 0 {
   890  		return metadata.StagedMetadatas{nextMetadata}
   891  	}
   892  	currCutoverNanos := currMetadatas[len(currMetadatas)-1].CutoverNanos
   893  	if currCutoverNanos > nextMetadata.CutoverNanos {
   894  		nextMetadata.CutoverNanos = nextCutoverNanos
   895  	}
   896  	currMetadatas = append(currMetadatas, nextMetadata)
   897  	return currMetadatas
   898  }
   899  
   900  // mergeResultsForNewRollupIDs merges the current list of staged metadatas for new rollup IDs
   901  // with the list of staged metadatas for new rollup IDs at the next rule cutover time, assuming
   902  // that both the current metadatas list and the next metadatas list are sorted by rollup IDs
   903  // in ascending order.
   904  // NB: each item in the `nextResults` array has a single staged metadata in the `metadatas` array
   905  // as the staged metadata for the associated rollup ID at the next cutover time.
   906  func mergeResultsForNewRollupIDs(
   907  	currResults []IDWithMetadatas,
   908  	nextResults []IDWithMetadatas,
   909  	nextCutoverNanos int64,
   910  ) []IDWithMetadatas {
   911  	var (
   912  		currLen, nextLen = len(currResults), len(nextResults)
   913  		currIdx, nextIdx int
   914  	)
   915  	for currIdx < currLen || nextIdx < nextLen {
   916  		var compareResult int
   917  		if currIdx >= currLen {
   918  			compareResult = 1
   919  		} else if nextIdx >= nextLen {
   920  			compareResult = -1
   921  		} else {
   922  			compareResult = bytes.Compare(currResults[currIdx].ID, nextResults[nextIdx].ID)
   923  		}
   924  
   925  		// If the current result and the next result have the same ID, we append the next metadata
   926  		// to the end of the metadata list.
   927  		if compareResult == 0 {
   928  			currResults[currIdx].Metadatas = append(currResults[currIdx].Metadatas, nextResults[nextIdx].Metadatas[0])
   929  			currIdx++
   930  			nextIdx++
   931  			continue
   932  		}
   933  
   934  		// If the current ID is smaller, it means the current rollup ID is tombstoned at the next
   935  		// cutover time.
   936  		if compareResult < 0 {
   937  			tombstonedMetadata := metadata.StagedMetadata{CutoverNanos: nextCutoverNanos, Tombstoned: true}
   938  			currResults[currIdx].Metadatas = append(currResults[currIdx].Metadatas, tombstonedMetadata)
   939  			currIdx++
   940  			continue
   941  		}
   942  
   943  		// Otherwise the current ID is larger, meaning a new ID is added at the next cutover time.
   944  		currResults = append(currResults, nextResults[nextIdx])
   945  		nextIdx++
   946  	}
   947  	sort.Sort(IDWithMetadatasByIDAsc(currResults))
   948  	return currResults
   949  }
   950  
   951  type int64Asc []int64
   952  
   953  func (a int64Asc) Len() int           { return len(a) }
   954  func (a int64Asc) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
   955  func (a int64Asc) Less(i, j int) bool { return a[i] < a[j] }
   956  
   957  type matchRollupTargetOptions struct {
   958  	generateRollupID bool
   959  }
   960  
   961  type ruleMatchResults struct {
   962  	cutoverNanos int64
   963  	pipelines    []metadata.PipelineMetadata
   964  }
   965  
   966  // merge merges in another rule match results in place.
   967  func (res *ruleMatchResults) merge(other ruleMatchResults) *ruleMatchResults {
   968  	if res.cutoverNanos < other.cutoverNanos {
   969  		res.cutoverNanos = other.cutoverNanos
   970  	}
   971  	res.pipelines = append(res.pipelines, other.pipelines...)
   972  	return res
   973  }
   974  
   975  // unique de-duplicates the pipelines.
   976  func (res *ruleMatchResults) unique() *ruleMatchResults {
   977  	if len(res.pipelines) == 0 {
   978  		return res
   979  	}
   980  
   981  	// Otherwise merge as per usual
   982  	curr := 0
   983  	for i := 1; i < len(res.pipelines); i++ {
   984  		foundDup := false
   985  		for j := 0; j <= curr; j++ {
   986  			if res.pipelines[j].Equal(res.pipelines[i]) {
   987  				foundDup = true
   988  				break
   989  			}
   990  		}
   991  		if foundDup {
   992  			continue
   993  		}
   994  		curr++
   995  		res.pipelines[curr] = res.pipelines[i]
   996  	}
   997  	for i := curr + 1; i < len(res.pipelines); i++ {
   998  		res.pipelines[i] = metadata.PipelineMetadata{}
   999  	}
  1000  	res.pipelines = res.pipelines[:curr+1]
  1001  	return res
  1002  }
  1003  
  1004  // toStagedMetadata converts the match results to a staged metadata.
  1005  func (res *ruleMatchResults) toStagedMetadata() metadata.StagedMetadata {
  1006  	return metadata.StagedMetadata{
  1007  		CutoverNanos: res.cutoverNanos,
  1008  		Tombstoned:   false,
  1009  		Metadata:     metadata.Metadata{Pipelines: res.resolvedPipelines()},
  1010  	}
  1011  }
  1012  
  1013  func (res *ruleMatchResults) resolvedPipelines() []metadata.PipelineMetadata {
  1014  	if len(res.pipelines) > 0 {
  1015  		return res.pipelines
  1016  	}
  1017  	return metadata.DefaultPipelineMetadatas
  1018  }
  1019  
  1020  type idWithMatchResults struct {
  1021  	id           []byte
  1022  	matchResults ruleMatchResults
  1023  }
  1024  
  1025  type mappingResults struct {
  1026  	// This represent the match result that should be applied against the
  1027  	// incoming metric ID the mapping rules were matched against.
  1028  	forExistingID ruleMatchResults
  1029  }
  1030  
  1031  type rollupResults struct {
  1032  	// This represent the match result that should be applied against the
  1033  	// incoming metric ID the rollup rules were matched against. This usually contains
  1034  	// the match result produced by rollup rules containing rollup pipelines whose first
  1035  	// pipeline operation is not a rollup operation.
  1036  	forExistingID ruleMatchResults
  1037  
  1038  	// This represents the match result that should be applied against new rollup
  1039  	// IDs generated during the rule matching process. This usually contains
  1040  	// the match result produced by rollup rules containing rollup pipelines whose first
  1041  	// pipeline operation is a rollup operation.
  1042  	forNewRollupIDs []idWithMatchResults
  1043  
  1044  	// This represents whether or not the original (source) metric for the
  1045  	// matched rollup rule should be kept. If true, both metrics are written;
  1046  	// if false, only the new generated rollup metric is written.
  1047  	keepOriginal bool
  1048  }
  1049  
  1050  type forwardMatchResult struct {
  1051  	forExistingID   metadata.StagedMetadata
  1052  	forNewRollupIDs []IDWithMetadatas
  1053  	keepOriginal    bool
  1054  }