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  }