concourse/atc/engine/engine.go

270 lines
5.2 KiB
Go

package engine
import (
"context"
"sync"
"time"
"code.cloudfoundry.org/lager"
"code.cloudfoundry.org/lager/lagerctx"
"github.com/concourse/concourse/atc"
"github.com/concourse/concourse/atc/db"
"github.com/concourse/concourse/atc/exec"
"github.com/concourse/concourse/atc/metric"
)
//go:generate counterfeiter . Engine
type Engine interface {
NewBuild(db.Build) Runnable
ReleaseAll(lager.Logger)
}
//go:generate counterfeiter . Runnable
type Runnable interface {
Run(logger lager.Logger)
}
//go:generate counterfeiter . StepBuilder
type StepBuilder interface {
BuildStep(db.Build) (exec.Step, error)
}
func NewEngine(builder StepBuilder) Engine {
return &engine{
builder: builder,
release: make(chan bool),
trackedStates: new(sync.Map),
waitGroup: new(sync.WaitGroup),
}
}
type engine struct {
builder StepBuilder
release chan bool
trackedStates *sync.Map
waitGroup *sync.WaitGroup
}
func (engine *engine) ReleaseAll(logger lager.Logger) {
logger.Info("calling-release-on-builds")
close(engine.release)
logger.Info("waiting-on-builds")
engine.waitGroup.Wait()
logger.Info("finished-waiting-on-builds")
}
func (engine *engine) NewBuild(build db.Build) Runnable {
ctx, cancel := context.WithCancel(context.Background())
return NewBuild(
ctx,
cancel,
build,
engine.builder,
engine.release,
engine.trackedStates,
engine.waitGroup,
)
}
func NewBuild(
ctx context.Context,
cancel func(),
build db.Build,
builder StepBuilder,
release chan bool,
trackedStates *sync.Map,
waitGroup *sync.WaitGroup,
) Runnable {
return &engineBuild{
ctx: ctx,
cancel: cancel,
build: build,
builder: builder,
release: release,
trackedStates: trackedStates,
waitGroup: waitGroup,
}
}
type engineBuild struct {
ctx context.Context
cancel func()
build db.Build
builder StepBuilder
release chan bool
trackedStates *sync.Map
waitGroup *sync.WaitGroup
}
func (b *engineBuild) Run(logger lager.Logger) {
b.waitGroup.Add(1)
defer b.waitGroup.Done()
logger = logger.WithData(lager.Data{
"build": b.build.ID(),
"pipeline": b.build.PipelineName(),
"job": b.build.JobName(),
})
lock, acquired, err := b.build.AcquireTrackingLock(logger, time.Minute)
if err != nil {
logger.Error("failed-to-get-lock", err)
return
}
if !acquired {
logger.Debug("build-already-tracked")
return
}
defer lock.Release()
found, err := b.build.Reload()
if err != nil {
logger.Error("failed-to-load-build-from-db", err)
return
}
if !found {
logger.Info("build-not-found")
return
}
if !b.build.IsRunning() {
logger.Info("build-already-finished")
return
}
notifier, err := b.build.AbortNotifier()
if err != nil {
logger.Error("failed-to-listen-for-aborts", err)
return
}
defer notifier.Close()
step, err := b.builder.BuildStep(b.build)
if err != nil {
logger.Error("failed-to-build-step", err)
return
}
b.trackStarted(logger)
defer b.trackFinished(logger)
logger.Info("running")
state := b.runState()
defer b.clearRunState()
noleak := make(chan bool)
defer close(noleak)
go func() {
select {
case <-noleak:
case <-notifier.Notify():
logger.Info("aborting")
b.cancel()
}
}()
done := make(chan error)
go func() {
ctx := lagerctx.NewContext(b.ctx, logger)
done <- step.Run(ctx, state)
}()
select {
case <-b.release:
logger.Info("releasing")
case err = <-done:
b.finish(logger.Session("finish"), err, step.Succeeded())
}
}
func (b *engineBuild) finish(logger lager.Logger, err error, succeeded bool) {
if err == context.Canceled {
b.saveStatus(logger, atc.StatusAborted)
logger.Info("aborted")
} else if err != nil {
b.saveStatus(logger, atc.StatusErrored)
logger.Info("errored", lager.Data{"error": err.Error()})
} else if succeeded {
b.saveStatus(logger, atc.StatusSucceeded)
logger.Info("succeeded")
} else {
b.saveStatus(logger, atc.StatusFailed)
logger.Info("failed")
}
}
func (b *engineBuild) saveStatus(logger lager.Logger, status atc.BuildStatus) {
if err := b.build.Finish(db.BuildStatus(status)); err != nil {
logger.Error("failed-to-finish-build", err)
}
}
func (b *engineBuild) trackStarted(logger lager.Logger) {
metric.BuildStarted{
PipelineName: b.build.PipelineName(),
JobName: b.build.JobName(),
BuildName: b.build.Name(),
BuildID: b.build.ID(),
TeamName: b.build.TeamName(),
}.Emit(logger)
}
func (b *engineBuild) trackFinished(logger lager.Logger) {
found, err := b.build.Reload()
if err != nil {
logger.Error("failed-to-load-build-from-db", err)
return
}
if !found {
logger.Info("build-removed")
return
}
if !b.build.IsRunning() {
metric.BuildFinished{
PipelineName: b.build.PipelineName(),
JobName: b.build.JobName(),
BuildName: b.build.Name(),
BuildID: b.build.ID(),
BuildStatus: b.build.Status(),
BuildDuration: b.build.EndTime().Sub(b.build.StartTime()),
TeamName: b.build.TeamName(),
}.Emit(logger)
}
}
func (build *engineBuild) runState() exec.RunState {
existingState, _ := build.trackedStates.LoadOrStore(build.build.ID(), exec.NewRunState())
return existingState.(exec.RunState)
}
func (build *engineBuild) clearRunState() {
build.trackedStates.Delete(build.build.ID())
}