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) }