github.com/joomcode/cue@v0.4.4-0.20221111115225-539fe3512047/tools/flow/tasks.go (about)

     1  // Copyright 2020 CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package flow
    16  
    17  // This file contains functionality for identifying tasks in the configuration
    18  // and annotating the dependencies between them.
    19  
    20  import (
    21  	"github.com/joomcode/cue/cue"
    22  	"github.com/joomcode/cue/cue/errors"
    23  	"github.com/joomcode/cue/internal/core/adt"
    24  	"github.com/joomcode/cue/internal/core/dep"
    25  	"github.com/joomcode/cue/internal/value"
    26  )
    27  
    28  // initTasks takes the current configuration and adds tasks to the list of
    29  // tasks. It can be run multiple times on increasingly more concrete
    30  // configurations to add more tasks, whereby the task pointers of previously
    31  // found tasks are preserved.
    32  func (c *Controller) initTasks() {
    33  	// Clear previous cache.
    34  	c.nodes = map[*adt.Vertex]*Task{}
    35  
    36  	v := c.inst.LookupPath(c.cfg.Root)
    37  	if err := v.Err(); err != nil {
    38  		c.addErr(err, "invalid root")
    39  		c.cancel()
    40  		return
    41  	}
    42  
    43  	// Mark any task that is located under the root.
    44  	c.findRootTasks(v)
    45  
    46  	// Mark any tasks that are implied by dependencies.
    47  	// Note that the list of tasks may grow as this loop progresses.
    48  	for i := 0; i < len(c.tasks); i++ {
    49  		t := c.tasks[i]
    50  		c.markTaskDependencies(t, t.vertex())
    51  	}
    52  
    53  	// Check if there are cycles in the task dependencies.
    54  	if err := checkCycle(c.tasks); err != nil {
    55  		c.addErr(err, "cyclic task")
    56  	}
    57  
    58  	if c.errs != nil {
    59  		c.cancel()
    60  	}
    61  }
    62  
    63  // findRootTasks finds tasks under the root.
    64  func (c *Controller) findRootTasks(v cue.Value) {
    65  	t := c.getTask(nil, v)
    66  
    67  	if t != nil {
    68  		return
    69  	}
    70  
    71  	opts := []cue.Option{}
    72  
    73  	if c.cfg.FindHiddenTasks {
    74  		opts = append(opts, cue.Hidden(true), cue.Definitions(false))
    75  	}
    76  
    77  	for iter, _ := v.Fields(opts...); iter.Next(); {
    78  		c.findRootTasks(iter.Value())
    79  	}
    80  }
    81  
    82  // This file contains the functionality to locate and record the tasks of
    83  // a configuration. It:
    84  //   - create Task struct for each node that is a task
    85  //   - associate nodes in a configuration with a Task, if applicable.
    86  // The node-to-task map is used to determine task dependencies.
    87  
    88  // getTask finds and marks tasks that are descendents of v.
    89  func (c *Controller) getTask(scope *Task, v cue.Value) *Task {
    90  	// Look up cached node.
    91  	_, w := value.ToInternal(v)
    92  	if t, ok := c.nodes[w]; ok {
    93  		return t
    94  	}
    95  
    96  	// Look up cached task from previous evaluation.
    97  	p := v.Path()
    98  	key := p.String()
    99  
   100  	t := c.keys[key]
   101  
   102  	if t == nil {
   103  		r, err := c.isTask(v)
   104  
   105  		var errs errors.Error
   106  		if err != nil {
   107  			if !c.inRoot(w) {
   108  				// Must be in InferTask mode. In this case we ignore the error.
   109  				r = nil
   110  			} else {
   111  				c.addErr(err, "invalid task")
   112  				errs = errors.Promote(err, "create task")
   113  			}
   114  		}
   115  
   116  		if r != nil {
   117  			index := len(c.tasks)
   118  			t = &Task{
   119  				v:      v,
   120  				c:      c,
   121  				r:      r,
   122  				path:   p,
   123  				labels: w.Path(),
   124  				key:    key,
   125  				index:  index,
   126  				err:    errs,
   127  			}
   128  			c.tasks = append(c.tasks, t)
   129  			c.keys[key] = t
   130  		}
   131  	}
   132  
   133  	// Process nodes of task for this evaluation.
   134  	if t != nil {
   135  		scope = t
   136  		if t.state <= Ready {
   137  			// Don't set the value if the task is currently running as this may
   138  			// result in all kinds of inconsistency issues.
   139  			t.v = v
   140  		}
   141  
   142  		c.tagChildren(w, t)
   143  	}
   144  
   145  	c.nodes[w] = scope
   146  
   147  	return t
   148  }
   149  
   150  func (c *Controller) tagChildren(n *adt.Vertex, t *Task) {
   151  	for _, a := range n.Arcs {
   152  		c.nodes[a] = t
   153  		c.tagChildren(a, t)
   154  	}
   155  }
   156  
   157  // findImpliedTask determines the task of corresponding to node n, if any. If n
   158  // is not already associated with a task, it tries to determine whether n is
   159  // part of a task by checking if any of the parent nodes is a task.
   160  //
   161  // TODO: it is actually more accurate to check for tasks from top down. TODO:
   162  // What should be done if a subtasks is referenced that is embedded in another
   163  // task. Should the surrounding tasks be added as well?
   164  func (c *Controller) findImpliedTask(d dep.Dependency) *Task {
   165  	// Ignore references into packages. Fill will fundamentally not work for
   166  	// packages, and packages cannot point back to the main package as cycles
   167  	// are not allowed.
   168  	if d.Import() != nil {
   169  		return nil
   170  	}
   171  
   172  	n := d.Node
   173  
   174  	// This Finalize should not be necessary, as the input to dep is already
   175  	// finalized. However, cue cmd uses some legacy instance stitching code
   176  	// where some of the backlink Environments are not properly initialized.
   177  	// Finalizing should patch those up at the expense of doing some duplicate
   178  	// work. The plan is to replace `cue cmd` with a much more clean
   179  	// implementation (probably a separate tool called `cuerun`) where this
   180  	// issue is fixed. For now we leave this patch.
   181  	//
   182  	// Note that this issue predates package flow, but that it just surfaced in
   183  	// flow and having a different evaluation order.
   184  	//
   185  	// Note: this call is cheap if n is already Finalized.
   186  	n.Finalize(c.opCtx)
   187  
   188  	for ; n != nil; n = n.Parent {
   189  		if c.cfg.IgnoreConcrete && n.IsConcrete() {
   190  			if k := n.BaseValue.Kind(); k != adt.StructKind && k != adt.ListKind {
   191  				return nil
   192  			}
   193  		}
   194  
   195  		t, ok := c.nodes[n]
   196  		if ok || !c.cfg.InferTasks {
   197  			return t
   198  		}
   199  
   200  		if !d.IsRoot() {
   201  			v := value.Make(c.opCtx, n)
   202  
   203  			if t := c.getTask(nil, v); t != nil {
   204  				return t
   205  			}
   206  		}
   207  	}
   208  
   209  	return nil
   210  }
   211  
   212  // markTaskDependencies traces through all conjuncts of a Task and marks
   213  // any dependencies on other tasks.
   214  //
   215  // The dependencies for a node by traversing the nodes of a task and then
   216  // traversing the dependencies of the conjuncts.
   217  //
   218  // This terminates because:
   219  //
   220  //  - traversing all nodes of all tasks is guaranteed finite (CUE does not
   221  //    evaluate to infinite structure).
   222  //
   223  //  - traversing conjuncts of all nodes is finite, as the input syntax is
   224  //    inherently finite.
   225  //
   226  //  - as regular nodes are traversed recursively they are marked with a cycle
   227  //    marker to detect cycles, ensuring a finite traversal as well.
   228  //
   229  func (c *Controller) markTaskDependencies(t *Task, n *adt.Vertex) {
   230  	dep.VisitFields(c.opCtx, n, func(d dep.Dependency) error {
   231  		depTask := c.findImpliedTask(d)
   232  		if depTask != nil {
   233  			if depTask != cycleMarker {
   234  				v := value.Make(c.opCtx, d.Node)
   235  				t.addDep(v.Path().String(), depTask)
   236  			}
   237  			return nil
   238  		}
   239  
   240  		// If this points to a non-task node, it may itself point to a task.
   241  		// Handling this allows for dynamic references. For instance, such a
   242  		// value may reference the result value of a task, or even create
   243  		// new tasks based on the result of another task.
   244  		if d.Import() == nil {
   245  			c.nodes[d.Node] = cycleMarker
   246  			c.markTaskDependencies(t, d.Node)
   247  			c.nodes[d.Node] = nil
   248  		}
   249  		return nil
   250  	})
   251  }
   252  
   253  func (c *Controller) inRoot(n *adt.Vertex) bool {
   254  	path := value.Make(c.opCtx, n).Path().Selectors()
   255  	root := c.cfg.Root.Selectors()
   256  	if len(path) < len(root) {
   257  		return false
   258  	}
   259  	for i, sel := range root {
   260  		if path[i] != sel {
   261  			return false
   262  		}
   263  	}
   264  	return true
   265  }
   266  
   267  var cycleMarker = &Task{}