···4343 EventPayload string `xorm:"LONGTEXT"`
4444 TriggerEvent string // the trigger event defined in the `on` configuration of the triggered workflow
4545 Status Status `xorm:"index"`
4646+ Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed
4647 Started timeutil.TimeStamp
4748 Stopped timeutil.TimeStamp
4849 Created timeutil.TimeStamp `xorm:"created"`
···332333 return run, nil
333334}
334335336336+// UpdateRun updates a run.
337337+// It requires the inputted run has Version set.
338338+// It will return error if the version is not matched (it means the run has been changed after loaded).
335339func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
336340 sess := db.GetEngine(ctx).ID(run.ID)
337341 if len(cols) > 0 {
338342 sess.Cols(cols...)
339343 }
340340- _, err := sess.Update(run)
344344+ affected, err := sess.Update(run)
345345+ if err != nil {
346346+ return err
347347+ }
348348+ if affected == 0 {
349349+ return fmt.Errorf("run has changed")
350350+ // It's impossible that the run is not found, since Gitea never deletes runs.
351351+ }
341352342353 if run.Status != 0 || util.SliceContains(cols, "status") {
343354 if run.RepoID == 0 {
···358369 }
359370 }
360371361361- return err
372372+ return nil
362373}
363374364375type ActionRunIndex db.ResourceIndex
+24-15
models/actions/run_job.go
···114114 if affected != 0 && util.SliceContains(cols, "status") && job.Status.IsWaiting() {
115115 // if the status of job changes to waiting again, increase tasks version.
116116 if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil {
117117- return affected, err
117117+ return 0, err
118118 }
119119 }
120120121121 if job.RunID == 0 {
122122 var err error
123123 if job, err = GetRunJobByID(ctx, job.ID); err != nil {
124124- return affected, err
124124+ return 0, err
125125 }
126126 }
127127128128- jobs, err := GetRunJobsByRunID(ctx, job.RunID)
129129- if err != nil {
130130- return affected, err
128128+ {
129129+ // Other goroutines may aggregate the status of the run and update it too.
130130+ // So we need load the run and its jobs before updating the run.
131131+ run, err := GetRunByID(ctx, job.RunID)
132132+ if err != nil {
133133+ return 0, err
134134+ }
135135+ jobs, err := GetRunJobsByRunID(ctx, job.RunID)
136136+ if err != nil {
137137+ return 0, err
138138+ }
139139+ run.Status = aggregateJobStatus(jobs)
140140+ if run.Started.IsZero() && run.Status.IsRunning() {
141141+ run.Started = timeutil.TimeStampNow()
142142+ }
143143+ if run.Stopped.IsZero() && run.Status.IsDone() {
144144+ run.Stopped = timeutil.TimeStampNow()
145145+ }
146146+ if err := UpdateRun(ctx, run, "status", "started", "stopped"); err != nil {
147147+ return 0, fmt.Errorf("update run %d: %w", run.ID, err)
148148+ }
131149 }
132150133133- runStatus := aggregateJobStatus(jobs)
134134-135135- run := &ActionRun{
136136- ID: job.RunID,
137137- Status: runStatus,
138138- }
139139- if runStatus.IsDone() {
140140- run.Stopped = timeutil.TimeStampNow()
141141- }
142142- return affected, UpdateRun(ctx, run)
151151+ return affected, nil
143152}
144153145154func aggregateJobStatus(jobs []*ActionRunJob) Status {