github.com/benchkram/bob@v0.0.0-20240314204020-b7a57f2f9be9/bob/playbook/playbook.go (about) 1 package playbook 2 3 import ( 4 "bytes" 5 "fmt" 6 "runtime" 7 "sort" 8 "sync" 9 "time" 10 11 "github.com/benchkram/bob/bobtask" 12 "github.com/benchkram/bob/bobtask/buildinfo" 13 "github.com/benchkram/bob/bobtask/hash" 14 "github.com/benchkram/bob/pkg/boberror" 15 "github.com/benchkram/bob/pkg/store" 16 "github.com/benchkram/bob/pkg/usererror" 17 "github.com/benchkram/errz" 18 ) 19 20 // The playbook defines the order in which tasks are allowed to run. 21 // Also determines the possibility to run tasks in parallel. 22 23 var ErrDone = fmt.Errorf("playbook is done") 24 var ErrFailed = fmt.Errorf("playbook failed") 25 26 type Playbook struct { 27 // taskChannel is closed when the root 28 // task completes. 29 taskChannel chan *bobtask.Task 30 31 // errorChannel to transport errors to the caller 32 errorChannel chan error 33 34 // root task 35 root string 36 // rootID for optimized access 37 rootID int 38 39 Tasks StatusMap 40 // TasksOptimized uses a array instead of an map 41 TasksOptimized StatusSlice 42 43 namePad int 44 45 done bool 46 // doneChannel is closed when the playbook is done. 47 doneChannel chan struct{} 48 49 // start is the point in time the playbook started 50 start time.Time 51 52 // enableCaching allows artifacts to be read & written to a store. 53 // Default: true. 54 enableCaching bool 55 56 // predictedNumOfTasks is used to pick 57 // an appropriate channel size for the task queue. 58 predictedNumOfTasks int 59 60 // maxParallel is the maximum number of parallel executed tasks 61 maxParallel int 62 63 // remoteStore is the artifacts remote store 64 remoteStore store.Store 65 66 // localStore is the artifacts local store 67 localStore store.Store 68 69 // enablePush allows pushing artifacts to remote store 70 enablePush bool 71 72 // enablePull allows pulling artifacts from remote store 73 enablePull bool 74 75 // oncePrepareOptimizedAccess is used to initalize the optimized 76 // slice to access tasks. 77 oncePrepareOptimizedAccess sync.Once 78 } 79 80 func New(root string, rootID int, opts ...Option) *Playbook { 81 p := &Playbook{ 82 errorChannel: make(chan error), 83 Tasks: make(StatusMap), 84 TasksOptimized: make(StatusSlice, 0), 85 doneChannel: make(chan struct{}), 86 enableCaching: true, 87 root: root, 88 rootID: rootID, 89 90 maxParallel: runtime.NumCPU(), 91 92 predictedNumOfTasks: 100000, 93 } 94 95 for _, opt := range opts { 96 if opt == nil { 97 continue 98 } 99 opt(p) 100 } 101 102 // Try to make the task channel the same size as the number of tasks. 103 // (Matthias) There was a reason why this was neccessary, probably it's related 104 // to beeing able to shutdown the playbook correctly? Unsure! 105 p.taskChannel = make(chan *bobtask.Task, p.predictedNumOfTasks) 106 107 return p 108 } 109 110 type RebuildCause string 111 112 func (rc *RebuildCause) String() string { 113 return string(*rc) 114 } 115 116 const ( 117 InputNotFoundInBuildInfo RebuildCause = "input-not-in-build-info" // aka local cache miss 118 TaskForcedRebuild RebuildCause = "forced" 119 DependencyChanged RebuildCause = "dependency-changed" 120 TargetInvalid RebuildCause = "target-invalid" 121 TargetNotInLocalStore RebuildCause = "target-not-in-localstore" 122 ) 123 124 func (p *Playbook) DoneChan() chan struct{} { 125 return p.doneChannel 126 } 127 128 // TaskChannel returns the next task 129 func (p *Playbook) TaskChannel() <-chan *bobtask.Task { 130 return p.taskChannel 131 } 132 133 func (p *Playbook) ErrorChannel() <-chan error { 134 return p.errorChannel 135 } 136 137 func (p *Playbook) ExecutionTime() time.Duration { 138 return time.Since(p.start) 139 } 140 141 // TaskStatus returns the current state of a task 142 func (p *Playbook) TaskStatus(taskname string) (ts *Status, _ error) { 143 status, ok := p.Tasks[taskname] 144 if !ok { 145 return ts, usererror.Wrap(boberror.ErrTaskDoesNotExistF(taskname)) 146 } 147 return status, nil 148 } 149 150 // TaskCompleted sets a task to completed 151 func (p *Playbook) TaskCompleted(taskID int) (err error) { 152 defer errz.Recover(&err) 153 154 task := p.TasksOptimized[taskID] 155 156 buildInfo, err := p.computeBuildinfo(task.Name()) 157 errz.Fatal(err) 158 159 // Store buildinfo 160 err = p.storeBuildInfo(task.Name(), buildInfo) 161 errz.Fatal(err) 162 163 // Store targets in the artifact store 164 if p.enableCaching { 165 hashIn, err := task.HashIn() 166 errz.Fatal(err) 167 err = p.artifactCreate(task.Name(), hashIn) 168 errz.Fatal(err) 169 } 170 171 // update task state and trigger another playbook run 172 err = p.setTaskState(taskID, StateCompleted, nil) 173 errz.Fatal(err) 174 175 return nil 176 } 177 178 // TaskNoRebuildRequired sets a task's state to indicate that no rebuild is required 179 func (p *Playbook) TaskNoRebuildRequired(taskID int) (err error) { 180 defer errz.Recover(&err) 181 182 err = p.setTaskState(taskID, StateNoRebuildRequired, nil) 183 errz.Fatal(err) 184 185 return nil 186 } 187 188 // TaskFailed sets a task to failed 189 func (p *Playbook) TaskFailed(taskID int, taskErr error) (err error) { 190 defer errz.Recover(&err) 191 192 err = p.setTaskState(taskID, StateFailed, taskErr) 193 errz.Fatal(err) 194 195 return nil 196 } 197 198 // TaskCanceled sets a task to canceled 199 func (p *Playbook) TaskCanceled(taskID int) (err error) { 200 201 defer errz.Recover(&err) 202 203 err = p.setTaskState(taskID, StateCanceled, nil) 204 errz.Fatal(err) 205 206 return nil 207 } 208 209 func (p *Playbook) List() (err error) { 210 defer errz.Recover(&err) 211 212 keys := make([]string, 0, len(p.Tasks)) 213 for k := range p.Tasks { 214 keys = append(keys, k) 215 } 216 sort.Strings(keys) 217 218 for _, k := range keys { 219 fmt.Println(k) 220 } 221 222 return nil 223 } 224 225 func (p *Playbook) String() string { 226 description := bytes.NewBufferString("") 227 228 fmt.Fprint(description, "Playbook:\n") 229 230 keys := make([]string, 0, len(p.Tasks)) 231 for k := range p.Tasks { 232 keys = append(keys, k) 233 } 234 sort.Strings(keys) 235 236 for _, k := range keys { 237 task := p.Tasks[k] 238 fmt.Fprintf(description, " %s(%s): %s\n", k, task.Task.Name(), task.State()) 239 } 240 241 return description.String() 242 } 243 244 func (p *Playbook) setTaskState(taskID int, state State, taskError error) error { 245 task := p.TasksOptimized[taskID] 246 247 task.SetState(state, taskError) 248 switch state { 249 case StateCompleted, StateCanceled, StateNoRebuildRequired, StateFailed: 250 task.SetEnd(time.Now()) 251 } 252 253 return nil 254 } 255 256 func (p *Playbook) artifactCreate(taskname string, hash hash.In) error { 257 task, ok := p.Tasks[taskname] 258 if !ok { 259 return usererror.Wrap(boberror.ErrTaskDoesNotExistF(taskname)) 260 } 261 return task.Task.ArtifactCreate(hash) 262 } 263 264 func (p *Playbook) storeBuildInfo(taskname string, buildinfo *buildinfo.I) error { 265 task, ok := p.Tasks[taskname] 266 if !ok { 267 return usererror.Wrap(boberror.ErrTaskDoesNotExistF(taskname)) 268 } 269 270 return task.Task.WriteBuildinfo(buildinfo) 271 }