132 lines
3.4 KiB
Go
132 lines
3.4 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type PipelineRunID string
|
|
|
|
type PipelineRunStatus string
|
|
|
|
const (
|
|
PipelineRunRunning PipelineRunStatus = "running"
|
|
PipelineRunSucceeded PipelineRunStatus = "succeeded"
|
|
PipelineRunFailed PipelineRunStatus = "failed"
|
|
)
|
|
|
|
type PipelineRunRecord struct {
|
|
ID PipelineRunID `json:"id"`
|
|
PipelineID string `json:"pipeline_id"`
|
|
Status PipelineRunStatus `json:"status"`
|
|
StartedAt time.Time `json:"started_at"`
|
|
FinishedAt *time.Time `json:"finished_at,omitempty"`
|
|
Report RunReport `json:"report,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
type DuplicatePipelineRunError struct {
|
|
PipelineID string
|
|
RunID PipelineRunID
|
|
}
|
|
|
|
func (err DuplicatePipelineRunError) Error() string {
|
|
if err.RunID == "" {
|
|
return fmt.Sprintf("pipeline %q already has an active run", err.PipelineID)
|
|
}
|
|
return fmt.Sprintf("pipeline %q already has active run %s", err.PipelineID, err.RunID)
|
|
}
|
|
|
|
func IsDuplicatePipelineRun(err error) bool {
|
|
var duplicate DuplicatePipelineRunError
|
|
return errors.As(err, &duplicate)
|
|
}
|
|
|
|
type PipelineRunCoordinator struct {
|
|
ctx context.Context
|
|
run pipelineRunFunc
|
|
now func() time.Time
|
|
mu sync.Mutex
|
|
nextID uint64
|
|
active map[string]PipelineRunRecord
|
|
}
|
|
|
|
type pipelineRunFunc func(context.Context, RunPipelineOptions) (RunReport, error)
|
|
|
|
func NewPipelineRunCoordinator(ctx context.Context) *PipelineRunCoordinator {
|
|
return newPipelineRunCoordinator(ctx, RunPipeline)
|
|
}
|
|
|
|
func newPipelineRunCoordinator(ctx context.Context, run pipelineRunFunc) *PipelineRunCoordinator {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
return &PipelineRunCoordinator{
|
|
ctx: ctx,
|
|
run: run,
|
|
now: time.Now,
|
|
active: map[string]PipelineRunRecord{},
|
|
}
|
|
}
|
|
|
|
func (coordinator *PipelineRunCoordinator) RunPipeline(ctx context.Context, options RunPipelineOptions) (PipelineRunRecord, error) {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return PipelineRunRecord{}, err
|
|
}
|
|
record, err := coordinator.admit(options.PipelineID)
|
|
if err != nil {
|
|
return PipelineRunRecord{}, err
|
|
}
|
|
defer coordinator.clear(options.PipelineID)
|
|
|
|
report, runErr := coordinator.run(coordinator.ctx, options)
|
|
record.Report = report
|
|
finishedAt := coordinator.now().UTC()
|
|
record.FinishedAt = &finishedAt
|
|
if runErr != nil {
|
|
record.Status = PipelineRunFailed
|
|
record.Error = runErr.Error()
|
|
return record, runErr
|
|
}
|
|
record.Status = PipelineRunSucceeded
|
|
return record, nil
|
|
}
|
|
|
|
func (coordinator *PipelineRunCoordinator) admit(pipelineID string) (PipelineRunRecord, error) {
|
|
coordinator.mu.Lock()
|
|
defer coordinator.mu.Unlock()
|
|
if active, ok := coordinator.active[pipelineID]; ok {
|
|
return PipelineRunRecord{}, DuplicatePipelineRunError{
|
|
PipelineID: pipelineID,
|
|
RunID: active.ID,
|
|
}
|
|
}
|
|
coordinator.nextID++
|
|
record := PipelineRunRecord{
|
|
ID: PipelineRunID(fmt.Sprintf("run-%016d", coordinator.nextID)),
|
|
PipelineID: pipelineID,
|
|
Status: PipelineRunRunning,
|
|
StartedAt: coordinator.now().UTC(),
|
|
}
|
|
coordinator.active[pipelineID] = record
|
|
return record, nil
|
|
}
|
|
|
|
func (coordinator *PipelineRunCoordinator) clear(pipelineID string) {
|
|
coordinator.mu.Lock()
|
|
defer coordinator.mu.Unlock()
|
|
delete(coordinator.active, pipelineID)
|
|
}
|
|
|
|
func (coordinator *PipelineRunCoordinator) activeCount() int {
|
|
coordinator.mu.Lock()
|
|
defer coordinator.mu.Unlock()
|
|
return len(coordinator.active)
|
|
}
|