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 }