Skip to content

Commit e8d33dc

Browse files
committed
feat(tasks): stop all tasks
1 parent 7023c11 commit e8d33dc

File tree

9 files changed

+423
-27
lines changed

9 files changed

+423
-27
lines changed

.dredd/hooks/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ func main() {
126126
h.Before("template > /api/project/{project_id}/templates/{template_id} > Get template > 200 > application/json", capabilityWrapper("template"))
127127
h.Before("template > /api/project/{project_id}/templates/{template_id} > Updates template > 204 > application/json", capabilityWrapper("template"))
128128
h.Before("template > /api/project/{project_id}/templates/{template_id} > Removes template > 204 > application/json", capabilityWrapper("template"))
129+
h.Before("template > /api/project/{project_id}/templates/{template_id}/stop_all_tasks > Stop all active tasks of template > 204 > application/json", capabilityWrapper("template"))
129130

130131
h.Before("task > /api/project/{project_id}/tasks > Starts a job > 201 > application/json", capabilityWrapper("template"))
131132
h.Before("task > /api/project/{project_id}/tasks/last > Get last 200 Tasks related to current project > 200 > application/json", capabilityWrapper("template"))

api-docs.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2437,6 +2437,25 @@ paths:
24372437
204:
24382438
description: template removed
24392439

2440+
/project/{project_id}/templates/{template_id}/stop_all_tasks:
2441+
parameters:
2442+
- $ref: "#/parameters/project_id"
2443+
- $ref: "#/parameters/template_id"
2444+
post:
2445+
tags:
2446+
- template
2447+
summary: Stop all active tasks of template
2448+
responses:
2449+
201:
2450+
description: tasks stopped
2451+
delete:
2452+
tags:
2453+
- template
2454+
summary: Removes template
2455+
responses:
2456+
204:
2457+
description: template removed
2458+
24402459

24412460
# project schedules
24422461
/project/{project_id}/schedules/{schedule_id}:

api/projects/tasks.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ import (
44
"bytes"
55
"encoding/json"
66
"errors"
7+
"net/http"
8+
"strconv"
9+
"time"
10+
711
"github.com/semaphoreui/semaphore/api/helpers"
812
"github.com/semaphoreui/semaphore/db"
913
"github.com/semaphoreui/semaphore/pkg/common_errors"
1014
"github.com/semaphoreui/semaphore/services/tasks"
1115
"github.com/semaphoreui/semaphore/util"
1216
log "github.com/sirupsen/logrus"
13-
"net/http"
14-
"strconv"
15-
"time"
1617
)
1718

1819
type TaskController struct {
@@ -412,5 +413,19 @@ func GetTaskStats(w http.ResponseWriter, r *http.Request) {
412413
}
413414

414415
func (c *TaskController) StopAllTasks(w http.ResponseWriter, r *http.Request) {
416+
project := helpers.GetFromContext(r, "project").(db.Project)
417+
tpl := helpers.GetFromContext(r, "template").(db.Template)
418+
419+
var stopObj struct {
420+
Force bool `json:"force"`
421+
}
422+
423+
// optional body; ignore bind error and default Force=false
424+
if ok := helpers.Bind(w, r, &stopObj); !ok {
425+
helpers.WriteErrorStatus(w, "Not allowed", http.StatusBadRequest)
426+
return
427+
}
428+
429+
taskPool(r).StopTasksByTemplate(project.ID, tpl.ID, stopObj.Force)
415430
w.WriteHeader(http.StatusNoContent)
416431
}

db/Store.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type RetrieveQueryParams struct {
5252
SortInverted bool
5353
Filter string
5454
Ownership OwnershipFilter
55+
TaskFilter *TaskFilter
5556
}
5657

5758
type ObjectReferrer struct {
@@ -171,6 +172,7 @@ type TaskFilter struct {
171172
Start *time.Time `json:"start"`
172173
End *time.Time `json:"end"`
173174
UserID *int `json:"user_id"`
175+
Status []task_logger.TaskStatus
174176
}
175177

176178
type TaskStat struct {

db/sql/task.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ package sql
22

33
import (
44
"encoding/json"
5-
"github.com/Masterminds/squirrel"
6-
"github.com/semaphoreui/semaphore/db"
75
"math/rand"
86
"time"
7+
8+
"github.com/Masterminds/squirrel"
9+
"github.com/semaphoreui/semaphore/db"
910
)
1011

1112
func (d *SqlDb) CreateTaskStage(stage db.TaskStage) (res db.TaskStage, err error) {
@@ -243,6 +244,10 @@ func (d *SqlDb) getTasks(projectID int, templateID *int, taskIDs []int, params d
243244
LeftJoin("`user` on task.user_id=`user`.id").
244245
OrderBy("id desc")
245246

247+
if params.TaskFilter != nil && len(params.TaskFilter.Status) > 0 {
248+
q = q.Where(squirrel.Eq{"status": params.TaskFilter.Status})
249+
}
250+
246251
if templateID == nil {
247252
q = q.Where("tpl.project_id=?", projectID)
248253
} else {

pkg/task_logger/task_logger.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ const (
2020
TaskFailStatus TaskStatus = "error"
2121
)
2222

23+
func UnfinishedTaskStatuses() []TaskStatus {
24+
return []TaskStatus{
25+
TaskWaitingStatus,
26+
TaskStartingStatus,
27+
TaskWaitingConfirmation,
28+
TaskConfirmed,
29+
TaskRejected,
30+
TaskRunningStatus,
31+
TaskStoppingStatus,
32+
}
33+
}
34+
2335
func (s TaskStatus) IsValid() bool {
2436
switch s {
2537
case TaskWaitingStatus,

services/tasks/TaskPool.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,81 @@ func (p *TaskPool) StopTask(targetTask db.Task, forceStop bool) error {
422422
return nil
423423
}
424424

425+
// StopTasksByTemplate stops all active (queued or running) tasks that belong to
426+
// the specified project and template. If forceStop is true, tasks are marked as
427+
// stopped immediately and running tasks are killed; otherwise tasks are marked
428+
// as stopping and will gracefully transition to stopped.
429+
func (p *TaskPool) StopTasksByTemplate(projectID int, templateID int, forceStop bool) {
430+
// Handle queued tasks
431+
for _, t := range p.state.QueueRange() {
432+
if t == nil {
433+
continue
434+
}
435+
if t.Task.ProjectID != projectID || t.Task.TemplateID != templateID {
436+
continue
437+
}
438+
if t.Task.Status.IsFinished() {
439+
continue
440+
}
441+
if forceStop {
442+
t.SetStatus(task_logger.TaskStoppedStatus)
443+
} else {
444+
t.SetStatus(task_logger.TaskStoppingStatus)
445+
}
446+
// Queued tasks will be dequeued and immediately finalize to Stopped in run()
447+
}
448+
449+
// Handle running tasks
450+
for _, t := range p.state.RunningRange() {
451+
if t == nil {
452+
continue
453+
}
454+
if t.Task.ProjectID != projectID || t.Task.TemplateID != templateID {
455+
continue
456+
}
457+
if t.Task.Status.IsFinished() {
458+
continue
459+
}
460+
prevStatus := t.Task.Status
461+
if forceStop {
462+
t.SetStatus(task_logger.TaskStoppedStatus)
463+
} else {
464+
t.SetStatus(task_logger.TaskStoppingStatus)
465+
}
466+
if prevStatus == task_logger.TaskRunningStatus {
467+
t.kill()
468+
}
469+
}
470+
471+
// Update tasks in DB that are neither queued nor running but still active
472+
// (e.g., created but not present in this instance's memory state).
473+
if tasks, err := p.store.GetTemplateTasks(projectID, templateID, db.RetrieveQueryParams{
474+
TaskFilter: &db.TaskFilter{
475+
Status: task_logger.UnfinishedTaskStatuses(),
476+
},
477+
}); err == nil {
478+
for _, twt := range tasks {
479+
480+
// if task is managed locally (queued/running), it was handled above
481+
if p.GetTask(twt.Task.ID) != nil {
482+
continue
483+
}
484+
485+
// mark non-local task as stopped and write event for history
486+
tr := NewTaskRunner(twt.Task, p, "", p.keyInstallationService)
487+
if err := tr.populateDetails(); err != nil {
488+
log.Error(err)
489+
continue
490+
}
491+
492+
tr.SetStatus(task_logger.TaskStoppedStatus)
493+
tr.createTaskEvent()
494+
}
495+
} else {
496+
log.Error(err)
497+
}
498+
}
499+
425500
// GetQueuedTasks returns a snapshot of tasks currently queued
426501
func (p *TaskPool) GetQueuedTasks() []*TaskRunner {
427502
return p.state.QueueRange()

0 commit comments

Comments
 (0)