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

     1  // Copyright (c) 2018 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 plan
    22  
    23  import (
    24  	"fmt"
    25  	"time"
    26  
    27  	"github.com/m3db/m3/src/query/executor/transform"
    28  	"github.com/m3db/m3/src/query/models"
    29  	"github.com/m3db/m3/src/query/parser"
    30  )
    31  
    32  // PhysicalPlan represents the physical plan.
    33  type PhysicalPlan struct {
    34  	steps            map[parser.NodeID]LogicalStep
    35  	pipeline         []parser.NodeID // Ordered list of steps to be performed
    36  	ResultStep       ResultOp
    37  	TimeSpec         transform.TimeSpec
    38  	Debug            bool
    39  	BlockType        models.FetchedBlockType
    40  	LookbackDuration time.Duration
    41  }
    42  
    43  // ResultOp is responsible for delivering results to the clients.
    44  type ResultOp struct {
    45  	Parent parser.NodeID
    46  }
    47  
    48  // NewPhysicalPlan is used to generate a physical plan.
    49  // Its responsibilities include creating consolidation nodes, result nodes,
    50  // pushing down predicates, and changing the ordering for nodes.
    51  // nolint: unparam
    52  func NewPhysicalPlan(
    53  	lp LogicalPlan,
    54  	params models.RequestParams,
    55  ) (PhysicalPlan, error) {
    56  	if params.Step <= 0 {
    57  		return PhysicalPlan{}, fmt.Errorf("expected non-zero step size, got %d",
    58  			params.Step)
    59  	}
    60  
    61  	// generate a new physical plan after cloning the logical plan so that any
    62  	// changes here do not update the logical plan.
    63  	cloned := lp.Clone()
    64  	p := PhysicalPlan{
    65  		steps:    cloned.Steps,
    66  		pipeline: cloned.Pipeline,
    67  		TimeSpec: transform.TimeSpec{
    68  			Start: params.Start,
    69  			End:   params.ExclusiveEnd(),
    70  			Now:   params.Now,
    71  			Step:  params.Step,
    72  		},
    73  		Debug:            params.Debug,
    74  		BlockType:        params.BlockType,
    75  		LookbackDuration: params.LookbackDuration,
    76  	}
    77  
    78  	pl, err := p.createResultNode()
    79  	if err != nil {
    80  		return PhysicalPlan{}, err
    81  	}
    82  
    83  	// Update times
    84  	pl = pl.shiftTime()
    85  	return pl, nil
    86  }
    87  
    88  func (p PhysicalPlan) shiftTime() PhysicalPlan {
    89  	var maxRange time.Duration
    90  
    91  	for _, transformID := range p.pipeline {
    92  		node := p.steps[transformID]
    93  		boundOp, ok := node.Transform.Op.(transform.BoundOp)
    94  		if !ok {
    95  			continue
    96  		}
    97  
    98  		spec := boundOp.Bounds()
    99  
   100  		if spec.Range > maxRange {
   101  			maxRange = spec.Range
   102  		}
   103  	}
   104  
   105  	startShift := p.LookbackDuration
   106  	if maxRange > 0 {
   107  		startShift = maxRange
   108  	}
   109  
   110  	remainder := startShift % p.TimeSpec.Step
   111  	var extraShift time.Duration
   112  	if remainder != 0 {
   113  		// Align the shift to be divisible by step.
   114  		extraShift = p.TimeSpec.Step - remainder
   115  	}
   116  
   117  	alignedShift := startShift + extraShift
   118  
   119  	p.TimeSpec.Start = p.TimeSpec.Start.Add(-1 * alignedShift)
   120  
   121  	return p
   122  }
   123  
   124  func (p PhysicalPlan) createResultNode() (PhysicalPlan, error) {
   125  	leaf, err := p.leafNode()
   126  	if err != nil {
   127  		return p, err
   128  	}
   129  
   130  	p.ResultStep = ResultOp{Parent: leaf.ID()}
   131  	return p, nil
   132  }
   133  
   134  func (p PhysicalPlan) leafNode() (LogicalStep, error) {
   135  	var leaf LogicalStep
   136  	found := false
   137  	for _, transformID := range p.pipeline {
   138  		node, ok := p.steps[transformID]
   139  		if !ok {
   140  			return leaf, fmt.Errorf("transform not found, %s", transformID)
   141  		}
   142  
   143  		if len(node.Children) == 0 {
   144  			if found {
   145  				return leaf, fmt.Errorf("multiple leaf nodes found, %v - %v", leaf, node)
   146  			}
   147  
   148  			leaf = node
   149  			found = true
   150  		}
   151  	}
   152  
   153  	return leaf, nil
   154  }
   155  
   156  // Step gets the logical step using its unique ID in the DAG.
   157  func (p PhysicalPlan) Step(ID parser.NodeID) (LogicalStep, bool) {
   158  	// Editor complains when inlining the map get
   159  	step, ok := p.steps[ID]
   160  	return step, ok
   161  }
   162  
   163  // String representation of the physical plan.
   164  func (p PhysicalPlan) String() string {
   165  	return fmt.Sprintf("StepCount: %s, Pipeline: %s, Result: %s, TimeSpec: %v",
   166  		p.steps, p.pipeline, p.ResultStep, p.TimeSpec)
   167  }