cuelang.org/go@v0.13.0/tools/flow/run.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 logic for running tasks.
    18  //
    19  // This implementation anticipates that workflows can also be used for defining
    20  // servers, not just batch scripts. In the future, tasks may be long running and
    21  // provide streams of results.
    22  //
    23  // The implementation starts a goroutine for each user-defined task, instead of
    24  // having a fixed pool of workers. The main reason for this is that tasks are
    25  // inherently heterogeneous and may be blocking on top of that. Also, in the
    26  // future tasks may be long running, as discussed above.
    27  
    28  import (
    29  	"fmt"
    30  	"os"
    31  	"slices"
    32  
    33  	"cuelang.org/go/cue/errors"
    34  	"cuelang.org/go/internal/core/adt"
    35  	"cuelang.org/go/internal/core/eval"
    36  	"cuelang.org/go/internal/cuedebug"
    37  	"cuelang.org/go/internal/value"
    38  )
    39  
    40  func (c *Controller) runLoop() {
    41  	_, root := value.ToInternal(c.inst)
    42  
    43  	// Copy the initial conjuncts.
    44  	var rootConjuncts []adt.Conjunct
    45  	root.VisitLeafConjuncts(func(c adt.Conjunct) bool {
    46  		rootConjuncts = append(rootConjuncts, c)
    47  		return true
    48  	})
    49  	n := len(rootConjuncts)
    50  	c.conjuncts = make([]adt.Conjunct, n, n+len(c.tasks))
    51  	copy(c.conjuncts, rootConjuncts)
    52  
    53  	c.markReady(nil)
    54  
    55  	for c.errs == nil {
    56  		// Dispatch all unblocked tasks to workers. Only update
    57  		// the configuration when all have been dispatched.
    58  
    59  		waiting := false
    60  		running := false
    61  
    62  		// Mark tasks as Ready.
    63  		for _, t := range c.tasks {
    64  			switch t.state {
    65  			case Waiting:
    66  				waiting = true
    67  
    68  			case Ready:
    69  				running = true
    70  
    71  				t.state = Running
    72  				c.updateTaskValue(t)
    73  
    74  				t.ctxt = eval.NewContext(value.ToInternal(t.v))
    75  
    76  				go func(t *Task) {
    77  					if err := t.r.Run(t, nil); err != nil {
    78  						t.err = errors.Promote(err, "task failed")
    79  					}
    80  
    81  					t.c.taskCh <- t
    82  				}(t)
    83  
    84  			case Running:
    85  				running = true
    86  
    87  			case Terminated:
    88  			}
    89  		}
    90  
    91  		if !running {
    92  			if waiting {
    93  				// Should not happen ever, as cycle detection should have caught
    94  				// this. But keep this around as a defensive measure.
    95  				c.addErr(errors.New("deadlock"), "run loop")
    96  			}
    97  			break
    98  		}
    99  
   100  		select {
   101  		case <-c.context.Done():
   102  			return
   103  
   104  		case t := <-c.taskCh:
   105  			t.state = Terminated
   106  
   107  			taskStats := *t.ctxt.Stats()
   108  			t.stats.Add(taskStats)
   109  			c.taskStats.Add(taskStats)
   110  
   111  			start := *c.opCtx.Stats()
   112  
   113  			switch t.err {
   114  			case nil:
   115  				c.updateTaskResults(t)
   116  
   117  			case ErrAbort:
   118  				// TODO: do something cleverer.
   119  				fallthrough
   120  
   121  			default:
   122  				c.addErr(t.err, "task failure")
   123  				return
   124  			}
   125  
   126  			// Recompute the configuration, if necessary.
   127  			if c.updateValue() {
   128  				// initTasks was already called in New to catch initialization
   129  				// errors earlier and add stats.
   130  				c.initTasks(false)
   131  			}
   132  
   133  			c.updateTaskValue(t)
   134  
   135  			t.stats.Add(c.opCtx.Stats().Since(start))
   136  
   137  			c.markReady(t)
   138  		}
   139  	}
   140  }
   141  
   142  func (c *Controller) markReady(t *Task) {
   143  	for _, x := range c.tasks {
   144  		if x.state == Waiting && x.isReady() {
   145  			x.state = Ready
   146  		}
   147  	}
   148  
   149  	cuedebug.Init()
   150  	if cuedebug.Flags.ToolsFlow {
   151  		fmt.Fprintln(os.Stderr, "tools/flow task dependency graph:")
   152  		fmt.Fprintln(os.Stderr, "```mermaid")
   153  		fmt.Fprint(os.Stderr, mermaidGraph(c))
   154  		fmt.Fprintln(os.Stderr, "```")
   155  	}
   156  
   157  	if c.cfg.UpdateFunc != nil {
   158  		if err := c.cfg.UpdateFunc(c, t); err != nil {
   159  			c.addErr(err, "task completed")
   160  			c.cancel()
   161  			return
   162  		}
   163  	}
   164  }
   165  
   166  // updateValue recomputes the workflow configuration if it is out of date. It
   167  // reports whether the values were updated.
   168  func (c *Controller) updateValue() bool {
   169  
   170  	if c.valueSeqNum == c.conjunctSeq {
   171  		return false
   172  	}
   173  
   174  	// TODO: incrementally update results. Currently, the entire tree is
   175  	// recomputed on every update. This should not be necessary with the right
   176  	// notification structure in place.
   177  
   178  	v := &adt.Vertex{Conjuncts: c.conjuncts}
   179  	v.Finalize(c.opCtx)
   180  
   181  	c.inst = value.Make(c.opCtx, v)
   182  	c.valueSeqNum = c.conjunctSeq
   183  	return true
   184  }
   185  
   186  // updateTaskValue updates the value of the task in the configuration if it is
   187  // out of date.
   188  func (c *Controller) updateTaskValue(t *Task) {
   189  	required := t.conjunctSeq
   190  	for _, dep := range t.depTasks {
   191  		if dep.conjunctSeq > required {
   192  			required = dep.conjunctSeq
   193  		}
   194  	}
   195  
   196  	if t.valueSeq == required {
   197  		return
   198  	}
   199  
   200  	if c.valueSeqNum < required {
   201  		c.updateValue()
   202  	}
   203  
   204  	t.v = c.inst.LookupPath(t.path)
   205  	t.valueSeq = required
   206  }
   207  
   208  // updateTaskResults updates the result status of the task and adds any result
   209  // values to the overall configuration.
   210  func (c *Controller) updateTaskResults(t *Task) bool {
   211  	if t.update == nil {
   212  		return false
   213  	}
   214  
   215  	expr := t.update
   216  	for _, label := range slices.Backward(t.labels) {
   217  		switch label.Typ() {
   218  		case adt.StringLabel, adt.HiddenLabel:
   219  			expr = &adt.StructLit{
   220  				Decls: []adt.Decl{
   221  					&adt.Field{
   222  						Label: label,
   223  						Value: expr,
   224  					},
   225  				},
   226  			}
   227  		case adt.IntLabel:
   228  			i := label.Index()
   229  			list := &adt.ListLit{}
   230  			any := &adt.Top{}
   231  			// TODO(perf): make this a constant thing. This will be possible with the query extension.
   232  			for range i {
   233  				list.Elems = append(list.Elems, any)
   234  			}
   235  			list.Elems = append(list.Elems, expr, &adt.Ellipsis{})
   236  			expr = list
   237  		default:
   238  			panic(fmt.Errorf("unexpected label type %v", label.Typ()))
   239  		}
   240  	}
   241  
   242  	t.update = nil
   243  
   244  	// TODO: replace rather than add conjunct if this task already added a
   245  	// conjunct before. This will allow for serving applications.
   246  	c.conjuncts = append(c.conjuncts, adt.MakeRootConjunct(c.env, expr))
   247  	c.conjunctSeq++
   248  	t.conjunctSeq = c.conjunctSeq
   249  
   250  	return true
   251  }