@@ -16,21 +16,21 @@ import (
1616	user_model "code.gitea.io/gitea/models/user" 
1717	"code.gitea.io/gitea/modules/git" 
1818	"code.gitea.io/gitea/modules/json" 
19+ 	"code.gitea.io/gitea/modules/log" 
1920	"code.gitea.io/gitea/modules/setting" 
2021	api "code.gitea.io/gitea/modules/structs" 
2122	"code.gitea.io/gitea/modules/timeutil" 
2223	"code.gitea.io/gitea/modules/util" 
2324	webhook_module "code.gitea.io/gitea/modules/webhook" 
2425
25- 	"github.com/nektos/act/pkg/jobparser" 
2626	"xorm.io/builder" 
2727)
2828
2929// ActionRun represents a run of a workflow file 
3030type  ActionRun  struct  {
3131	ID                 int64 
3232	Title              string 
33- 	RepoID             int64                   `xorm:"index  unique(repo_index)"` 
33+ 	RepoID             int64                   `xorm:"unique(repo_index) index(repo_concurrency )"` 
3434	Repo               * repo_model.Repository  `xorm:"-"` 
3535	OwnerID            int64                   `xorm:"index"` 
3636	WorkflowID         string                  `xorm:"index"`                     // the name of workflow file 
@@ -49,6 +49,9 @@ type ActionRun struct {
4949	TriggerEvent       string                        // the trigger event defined in the `on` configuration of the triggered workflow 
5050	Status             Status                        `xorm:"index"` 
5151	Version            int                           `xorm:"version default 0"`  // Status could be updated concomitantly, so an optimistic lock is needed 
52+ 	RawConcurrency     string                        // raw concurrency 
53+ 	ConcurrencyGroup   string                        `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"` 
54+ 	ConcurrencyCancel  bool                          `xorm:"NOT NULL DEFAULT FALSE"` 
5255	// Started and Stopped is used for recording last run time, if rerun happened, they will be reset to 0 
5356	Started  timeutil.TimeStamp 
5457	Stopped  timeutil.TimeStamp 
@@ -190,7 +193,7 @@ func (run *ActionRun) IsSchedule() bool {
190193	return  run .ScheduleID  >  0 
191194}
192195
193- func  updateRepoRunsNumbers (ctx  context.Context , repo  * repo_model.Repository ) error  {
196+ func  UpdateRepoRunsNumbers (ctx  context.Context , repo  * repo_model.Repository ) error  {
194197	_ , err  :=  db .GetEngine (ctx ).ID (repo .ID ).
195198		NoAutoTime ().
196199		SetExpr ("num_action_runs" ,
@@ -247,116 +250,62 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
247250			return  cancelledJobs , err 
248251		}
249252
250- 		// Iterate over each job and attempt to cancel it. 
251- 		for  _ , job  :=  range  jobs  {
252- 			// Skip jobs that are already in a terminal state (completed, cancelled, etc.). 
253- 			status  :=  job .Status 
254- 			if  status .IsDone () {
255- 				continue 
256- 			}
257- 
258- 			// If the job has no associated task (probably an error), set its status to 'Cancelled' and stop it. 
259- 			if  job .TaskID  ==  0  {
260- 				job .Status  =  StatusCancelled 
261- 				job .Stopped  =  timeutil .TimeStampNow ()
262- 
263- 				// Update the job's status and stopped time in the database. 
264- 				n , err  :=  UpdateRunJob (ctx , job , builder.Eq {"task_id" : 0 }, "status" , "stopped" )
265- 				if  err  !=  nil  {
266- 					return  cancelledJobs , err 
267- 				}
268- 
269- 				// If the update affected 0 rows, it means the job has changed in the meantime, so we need to try again. 
270- 				if  n  ==  0  {
271- 					return  cancelledJobs , errors .New ("job has changed, try again" )
272- 				}
273- 
274- 				cancelledJobs  =  append (cancelledJobs , job )
275- 				// Continue with the next job. 
276- 				continue 
277- 			}
278- 
279- 			// If the job has an associated task, try to stop the task, effectively cancelling the job. 
280- 			if  err  :=  StopTask (ctx , job .TaskID , StatusCancelled ); err  !=  nil  {
281- 				return  cancelledJobs , err 
282- 			}
283- 			cancelledJobs  =  append (cancelledJobs , job )
253+ 		cjs , err  :=  CancelJobs (ctx , jobs )
254+ 		if  err  !=  nil  {
255+ 			return  cancelledJobs , err 
284256		}
257+ 		cancelledJobs  =  append (cancelledJobs , cjs ... )
285258	}
286259
287260	// Return nil to indicate successful cancellation of all running and waiting jobs. 
288261	return  cancelledJobs , nil 
289262}
290263
291- // InsertRun inserts a run 
292- // The title will be cut off at 255 characters if it's longer than 255 characters. 
293- func  InsertRun (ctx  context.Context , run  * ActionRun , jobs  []* jobparser.SingleWorkflow ) error  {
294- 	return  db .WithTx (ctx , func (ctx  context.Context ) error  {
295- 		index , err  :=  db .GetNextResourceIndex (ctx , "action_run_index" , run .RepoID )
296- 		if  err  !=  nil  {
297- 			return  err 
264+ func  CancelJobs (ctx  context.Context , jobs  []* ActionRunJob ) ([]* ActionRunJob , error ) {
265+ 	cancelledJobs  :=  make ([]* ActionRunJob , 0 , len (jobs ))
266+ 	// Iterate over each job and attempt to cancel it. 
267+ 	for  _ , job  :=  range  jobs  {
268+ 		// Skip jobs that are already in a terminal state (completed, cancelled, etc.). 
269+ 		status  :=  job .Status 
270+ 		if  status .IsDone () {
271+ 			continue 
298272		}
299- 		run .Index  =  index 
300- 		run .Title  =  util .EllipsisDisplayString (run .Title , 255 )
301273
302- 		if  err  :=  db .Insert (ctx , run ); err  !=  nil  {
303- 			return  err 
304- 		}
274+ 		// If the job has no associated task (probably an error), set its status to 'Cancelled' and stop it. 
275+ 		if  job .TaskID  ==  0  {
276+ 			job .Status  =  StatusCancelled 
277+ 			job .Stopped  =  timeutil .TimeStampNow ()
305278
306- 		if   run . Repo   ==   nil  { 
307- 			repo , err  :=  repo_model . GetRepositoryByID (ctx , run . RepoID )
279+ 			 // Update the job's status and stopped time in the database. 
280+ 			n , err  :=  UpdateRunJob (ctx , job , builder. Eq { "task_id" :  0 },  "status" ,  "stopped" )
308281			if  err  !=  nil  {
309- 				return  err 
282+ 				return  cancelledJobs ,  err 
310283			}
311- 			run .Repo  =  repo 
312- 		}
313284
314- 		if  err  :=  updateRepoRunsNumbers (ctx , run .Repo ); err  !=  nil  {
315- 			return  err 
285+ 			// If the update affected 0 rows, it means the job has changed in the meantime 
286+ 			if  n  ==  0  {
287+ 				log .Error ("Failed to cancel job %d because it has changed" , job .ID )
288+ 				continue 
289+ 			}
290+ 
291+ 			cancelledJobs  =  append (cancelledJobs , job )
292+ 			// Continue with the next job. 
293+ 			continue 
316294		}
317295
318- 		runJobs  :=  make ([]* ActionRunJob , 0 , len (jobs ))
319- 		var  hasWaiting  bool 
320- 		for  _ , v  :=  range  jobs  {
321- 			id , job  :=  v .Job ()
322- 			needs  :=  job .Needs ()
323- 			if  err  :=  v .SetJob (id , job .EraseNeeds ()); err  !=  nil  {
324- 				return  err 
325- 			}
326- 			payload , _  :=  v .Marshal ()
327- 			status  :=  StatusWaiting 
328- 			if  len (needs ) >  0  ||  run .NeedApproval  {
329- 				status  =  StatusBlocked 
330- 			} else  {
331- 				hasWaiting  =  true 
332- 			}
333- 			job .Name  =  util .EllipsisDisplayString (job .Name , 255 )
334- 			runJobs  =  append (runJobs , & ActionRunJob {
335- 				RunID :             run .ID ,
336- 				RepoID :            run .RepoID ,
337- 				OwnerID :           run .OwnerID ,
338- 				CommitSHA :         run .CommitSHA ,
339- 				IsForkPullRequest : run .IsForkPullRequest ,
340- 				Name :              job .Name ,
341- 				WorkflowPayload :   payload ,
342- 				JobID :             id ,
343- 				Needs :             needs ,
344- 				RunsOn :            job .RunsOn (),
345- 				Status :            status ,
346- 			})
296+ 		// If the job has an associated task, try to stop the task, effectively cancelling the job. 
297+ 		if  err  :=  StopTask (ctx , job .TaskID , StatusCancelled ); err  !=  nil  {
298+ 			return  cancelledJobs , err 
347299		}
348- 		if  err  :=  db .Insert (ctx , runJobs ); err  !=  nil  {
349- 			return  err 
300+ 		updatedJob , err  :=  GetRunJobByID (ctx , job .ID )
301+ 		if  err  !=  nil  {
302+ 			return  cancelledJobs , fmt .Errorf ("get job: %w" , err )
350303		}
304+ 		cancelledJobs  =  append (cancelledJobs , updatedJob )
305+ 	}
351306
352- 		// if there is a job in the waiting status, increase tasks version. 
353- 		if  hasWaiting  {
354- 			if  err  :=  IncreaseTaskVersion (ctx , run .OwnerID , run .RepoID ); err  !=  nil  {
355- 				return  err 
356- 			}
357- 		}
358- 		return  nil 
359- 	})
307+ 	// Return nil to indicate successful cancellation of all running and waiting jobs. 
308+ 	return  cancelledJobs , nil 
360309}
361310
362311func  GetRunByRepoAndID (ctx  context.Context , repoID , runID  int64 ) (* ActionRun , error ) {
@@ -441,7 +390,7 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
441390		if  err  =  run .LoadRepo (ctx ); err  !=  nil  {
442391			return  err 
443392		}
444- 		if  err  :=  updateRepoRunsNumbers (ctx , run .Repo ); err  !=  nil  {
393+ 		if  err  :=  UpdateRepoRunsNumbers (ctx , run .Repo ); err  !=  nil  {
445394			return  err 
446395		}
447396	}
@@ -450,3 +399,59 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
450399}
451400
452401type  ActionRunIndex  db.ResourceIndex 
402+ 
403+ func  GetConcurrentRunsAndJobs (ctx  context.Context , repoID  int64 , concurrencyGroup  string , status  []Status ) ([]* ActionRun , []* ActionRunJob , error ) {
404+ 	runs , err  :=  db .Find [ActionRun ](ctx , & FindRunOptions {
405+ 		RepoID :           repoID ,
406+ 		ConcurrencyGroup : concurrencyGroup ,
407+ 		Status :           status ,
408+ 	})
409+ 	if  err  !=  nil  {
410+ 		return  nil , nil , fmt .Errorf ("find runs: %w" , err )
411+ 	}
412+ 
413+ 	jobs , err  :=  db .Find [ActionRunJob ](ctx , & FindRunJobOptions {
414+ 		RepoID :           repoID ,
415+ 		ConcurrencyGroup : concurrencyGroup ,
416+ 		Statuses :         status ,
417+ 	})
418+ 	if  err  !=  nil  {
419+ 		return  nil , nil , fmt .Errorf ("find jobs: %w" , err )
420+ 	}
421+ 
422+ 	return  runs , jobs , nil 
423+ }
424+ 
425+ func  CancelPreviousJobsByRunConcurrency (ctx  context.Context , actionRun  * ActionRun ) ([]* ActionRunJob , error ) {
426+ 	if  actionRun .ConcurrencyGroup  ==  ""  {
427+ 		return  nil , nil 
428+ 	}
429+ 
430+ 	var  jobsToCancel  []* ActionRunJob 
431+ 
432+ 	statusFindOption  :=  []Status {StatusWaiting , StatusBlocked }
433+ 	if  actionRun .ConcurrencyCancel  {
434+ 		statusFindOption  =  append (statusFindOption , StatusRunning )
435+ 	}
436+ 	runs , jobs , err  :=  GetConcurrentRunsAndJobs (ctx , actionRun .RepoID , actionRun .ConcurrencyGroup , statusFindOption )
437+ 	if  err  !=  nil  {
438+ 		return  nil , fmt .Errorf ("find concurrent runs and jobs: %w" , err )
439+ 	}
440+ 	jobsToCancel  =  append (jobsToCancel , jobs ... )
441+ 
442+ 	// cancel runs in the same concurrency group 
443+ 	for  _ , run  :=  range  runs  {
444+ 		if  run .ID  ==  actionRun .ID  {
445+ 			continue 
446+ 		}
447+ 		jobs , err  :=  db .Find [ActionRunJob ](ctx , FindRunJobOptions {
448+ 			RunID : run .ID ,
449+ 		})
450+ 		if  err  !=  nil  {
451+ 			return  nil , fmt .Errorf ("find run %d jobs: %w" , run .ID , err )
452+ 		}
453+ 		jobsToCancel  =  append (jobsToCancel , jobs ... )
454+ 	}
455+ 
456+ 	return  CancelJobs (ctx , jobsToCancel )
457+ }
0 commit comments