feat(be): add max parallel tasks to project settings and ability to suppress success alerts for tasks

This commit is contained in:
Denis Gukov 2022-02-12 17:15:15 +05:00
parent 7774378d8c
commit b127e054d8
11 changed files with 102 additions and 112 deletions

View File

@ -56,6 +56,7 @@ func GetMigrations() []Migration {
{Version: "2.8.39"},
{Version: "2.8.40"},
{Version: "2.8.42"},
{Version: "2.8.51"},
}
}

View File

@ -6,9 +6,10 @@ import (
// Project is the top level structure in Semaphore
type Project struct {
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name" binding:"required"`
Created time.Time `db:"created" json:"created"`
Alert bool `db:"alert" json:"alert"`
AlertChat *string `db:"alert_chat" json:"alert_chat"`
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name" binding:"required"`
Created time.Time `db:"created" json:"created"`
Alert bool `db:"alert" json:"alert"`
AlertChat *string `db:"alert_chat" json:"alert_chat"`
MaxParallelTasks int `db:"max_parallel_tasks" json:"max_parallel_tasks"`
}

View File

@ -4,13 +4,15 @@ import (
"time"
)
type TaskStatus string
const (
TaskRunningStatus = "running"
TaskWaitingStatus = "waiting"
TaskStoppingStatus = "stopping"
TaskStoppedStatus = "stopped"
TaskSuccessStatus = "success"
TaskFailStatus = "error"
TaskRunningStatus TaskStatus = "running"
TaskWaitingStatus TaskStatus = "waiting"
TaskStoppingStatus TaskStatus = "stopping"
TaskStoppedStatus TaskStatus = "stopped"
TaskSuccessStatus TaskStatus = "success"
TaskFailStatus TaskStatus = "error"
)
//Task is a model of a task which will be executed by the runner
@ -19,8 +21,8 @@ type Task struct {
TemplateID int `db:"template_id" json:"template_id" binding:"required"`
ProjectID int `db:"project_id" json:"project_id"`
Status string `db:"status" json:"status"`
Debug bool `db:"debug" json:"debug"`
Status TaskStatus `db:"status" json:"status"`
Debug bool `db:"debug" json:"debug"`
DryRun bool `db:"dry_run" json:"dry_run"`

View File

@ -71,6 +71,8 @@ type Template struct {
// Do not use it in your code. Use SurveyVars instead.
SurveyVarsJSON *string `db:"survey_vars" json:"-"`
SurveyVars []SurveyVar `db:"-" json:"survey_vars"`
SuppressSuccessAlerts bool `db:"suppress_success_alerts" json:"suppress_success_alerts"`
}
func (tpl *Template) Validate() error {

View File

@ -0,0 +1,2 @@
alter table `project` add column `max_parallel_tasks` int not null default 0;
alter table `project__template` add column `suppress_success_alerts` bool not null default false;

View File

@ -85,10 +85,11 @@ func (d *SqlDb) DeleteProject(projectID int) error {
func (d *SqlDb) UpdateProject(project db.Project) error {
_, err := d.exec(
"update project set name=?, alert=?, alert_chat=? where id=?",
"update project set name=?, alert=?, alert_chat=?, max_parallel_tasks=? where id=?",
project.Name,
project.Alert,
project.AlertChat,
project.MaxParallelTasks,
project.ID)
return err
}

View File

@ -17,8 +17,8 @@ func (d *SqlDb) CreateTemplate(template db.Template) (newTemplate db.Template, e
"id",
"insert into project__template (project_id, inventory_id, repository_id, environment_id, "+
"name, playbook, arguments, allow_override_args_in_task, description, vault_key_id, `type`, start_version,"+
"build_template_id, view_id, autorun, survey_vars)"+
"values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
"build_template_id, view_id, autorun, survey_vars, suppress_success_alerts)"+
"values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
template.ProjectID,
template.InventoryID,
template.RepositoryID,
@ -34,7 +34,8 @@ func (d *SqlDb) CreateTemplate(template db.Template) (newTemplate db.Template, e
template.BuildTemplateID,
template.ViewID,
template.Autorun,
db.ObjectToJSON(template.SurveyVars))
db.ObjectToJSON(template.SurveyVars),
template.SuppressSuccessAlerts)
if err != nil {
return
@ -74,7 +75,8 @@ func (d *SqlDb) UpdateTemplate(template db.Template) error {
"build_template_id=?, "+
"view_id=?, "+
"autorun=?, "+
"survey_vars=? "+
"survey_vars=?, "+
"suppress_success_alerts=? "+
"where id=? and project_id=?",
template.InventoryID,
template.RepositoryID,
@ -91,6 +93,7 @@ func (d *SqlDb) UpdateTemplate(template db.Template) error {
template.ViewID,
template.Autorun,
db.ObjectToJSON(template.SurveyVars),
template.SuppressSuccessAlerts,
template.ID,
template.ProjectID,
)

View File

@ -2,6 +2,7 @@ package tasks
import (
"bytes"
"github.com/ansible-semaphore/semaphore/db"
"html/template"
"net/http"
"strconv"
@ -71,6 +72,10 @@ func (t *TaskRunner) sendTelegramAlert() {
return
}
if t.template.SuppressSuccessAlerts && t.task.Status == db.TaskSuccessStatus {
return
}
chatID := util.Config.TelegramChat
if t.alertChat != nil && *t.alertChat != "" {
chatID = *t.alertChat
@ -106,7 +111,7 @@ func (t *TaskRunner) sendTelegramAlert() {
Name: t.template.Name,
TaskURL: util.Config.WebHost + "/project/" + strconv.Itoa(t.template.ProjectID) + "/templates/" + strconv.Itoa(t.template.ID) + "?t=" + strconv.Itoa(t.task.ID),
ChatID: chatID,
TaskResult: strings.ToUpper(t.task.Status),
TaskResult: strings.ToUpper(string(t.task.Status)),
TaskVersion: version,
TaskDescription: message,
Author: author,

View File

@ -17,18 +17,19 @@ type logRecord struct {
time time.Time
}
type resourceLock struct {
lock bool
holder *TaskRunner
}
type TaskPool struct {
// queue contains list of tasks in status TaskWaitingStatus.
queue []*TaskRunner
// register channel used to put tasks to queue.
register chan *TaskRunner
activeProj map[int]*TaskRunner
activeNodes map[string]*TaskRunner
// running is a number of task with status TaskRunningStatus.
running int
activeProj map[int]map[int]*TaskRunner
// runningTasks contains tasks with status TaskRunningStatus.
runningTasks map[int]*TaskRunner
@ -37,15 +38,10 @@ type TaskPool struct {
logger chan logRecord
store db.Store
}
type resourceLock struct {
lock bool
holder *TaskRunner
resourceLocker chan *resourceLock
}
var resourceLocker = make(chan *resourceLock)
func (p *TaskPool) GetTask(id int) (task *TaskRunner) {
for _, t := range p.queue {
@ -72,7 +68,7 @@ func (p *TaskPool) Run() {
ticker := time.NewTicker(5 * time.Second)
defer func() {
close(resourceLocker)
close(p.resourceLocker)
ticker.Stop()
}()
@ -86,33 +82,30 @@ func (p *TaskPool) Run() {
panic("Trying to lock an already locked resource!")
}
p.activeProj[t.task.ProjectID] = t
for _, node := range t.hosts {
p.activeNodes[node] = t
projTasks, ok := p.activeProj[t.task.ProjectID]
if !ok {
projTasks = make(map[int]*TaskRunner)
p.activeProj[t.task.ProjectID] = projTasks
}
p.running++
projTasks[t.task.ID] = t
p.runningTasks[t.task.ID] = t
continue
}
if p.activeProj[t.task.ProjectID] == t {
delete(p.activeProj, t.task.ProjectID)
if p.activeProj[t.task.ProjectID] != nil && p.activeProj[t.task.ProjectID][t.task.ID] != nil {
delete(p.activeProj[t.task.ProjectID], t.task.ID)
if len(p.activeProj[t.task.ProjectID]) == 0 {
delete(p.activeProj, t.task.ProjectID)
}
}
for _, node := range t.hosts {
delete(p.activeNodes, node)
}
p.running--
delete(p.runningTasks, t.task.ID)
}
}(resourceLocker)
}(p.resourceLocker)
for {
select {
case record := <-p.logger:
case record := <-p.logger: // new log message which should be put to database
_, err := record.task.pool.store.CreateTaskOutput(db.TaskOutput{
TaskID: record.task.task.ID,
Output: record.output,
@ -122,14 +115,15 @@ func (p *TaskPool) Run() {
if err != nil {
log.Error(err)
}
case task := <-p.register:
case task := <-p.register: // new task created by API or schedule
p.queue = append(p.queue, task)
log.Debug(task)
msg := "Task " + strconv.Itoa(task.task.ID) + " added to queue"
task.Log(msg)
log.Info(msg)
task.updateStatus()
case <-ticker.C:
case <-ticker.C: // timer 5 seconds
if len(p.queue) == 0 {
continue
}
@ -148,7 +142,7 @@ func (p *TaskPool) Run() {
continue
}
log.Info("Set resource locker with TaskRunner " + strconv.Itoa(t.task.ID))
resourceLocker <- &resourceLock{lock: true, holder: t}
p.resourceLocker <- &resourceLock{lock: true, holder: t}
if !t.prepared {
go t.prepareRun()
continue
@ -161,36 +155,40 @@ func (p *TaskPool) Run() {
}
func (p *TaskPool) blocks(t *TaskRunner) bool {
if p.running >= util.Config.MaxParallelTasks {
if len(p.runningTasks) >= util.Config.MaxParallelTasks {
return true
}
switch util.Config.ConcurrencyMode {
case "project":
return p.activeProj[t.task.ProjectID] != nil
case "node":
for _, node := range t.hosts {
if p.activeNodes[node] != nil {
return true
}
}
if p.activeProj[t.task.ProjectID] == nil || len(p.activeProj[t.task.ProjectID]) == 0 {
return false
default:
return p.running > 0
}
for _, r := range p.activeProj[t.task.ProjectID] {
if r.template.ID == t.task.TemplateID {
return true
}
}
proj, err := p.store.GetProject(t.task.ProjectID)
if err != nil {
log.Error(err)
return false
}
return proj.MaxParallelTasks > 0 && len(p.activeProj[t.task.ProjectID]) >= proj.MaxParallelTasks
}
func CreateTaskPool(store db.Store) TaskPool {
return TaskPool{
queue: make([]*TaskRunner, 0), // queue of waiting tasks
register: make(chan *TaskRunner), // add TaskRunner to queue
activeProj: make(map[int]*TaskRunner),
activeNodes: make(map[string]*TaskRunner),
running: 0, // number of running tasks
runningTasks: make(map[int]*TaskRunner), // working tasks
logger: make(chan logRecord, 10000), // store log records to database
store: store,
queue: make([]*TaskRunner, 0), // queue of waiting tasks
register: make(chan *TaskRunner), // add TaskRunner to queue
activeProj: make(map[int]map[int]*TaskRunner),
runningTasks: make(map[int]*TaskRunner), // working tasks
logger: make(chan logRecord, 10000), // store log records to database
store: store,
resourceLocker: make(chan *resourceLock),
}
}
@ -313,11 +311,20 @@ func (p *TaskPool) AddTask(taskObj db.Task, userID *int, projectID int) (newTask
return
}
p.register <- &TaskRunner{
taskRunner := TaskRunner{
task: newTask,
pool: p,
}
err = taskRunner.populateDetails()
if err != nil {
taskRunner.Log("Error: " + err.Error())
taskRunner.fail()
return
}
p.register <- &taskRunner
objType := db.EventTask
desc := "Task ID " + strconv.Itoa(newTask.ID) + " queued for running"
_, err = p.store.CreateEvent(db.Event{

View File

@ -30,9 +30,8 @@ type TaskRunner struct {
environment db.Environment
users []int
hosts []string
alertChat *string
alert bool
alertChat *string
prepared bool
process *os.Process
pool *TaskPool
@ -62,7 +61,7 @@ func (t *TaskRunner) getRepoPath() string {
return repo.GetFullPath()
}
func (t *TaskRunner) setStatus(status string) {
func (t *TaskRunner) setStatus(status db.TaskStatus) {
if t.task.Status == db.TaskStoppingStatus {
switch status {
case db.TaskFailStatus:
@ -132,7 +131,7 @@ func (t *TaskRunner) destroyKeys() {
func (t *TaskRunner) createTaskEvent() {
objType := db.EventTask
desc := "Task ID " + strconv.Itoa(t.task.ID) + " (" + t.template.Name + ")" + " finished - " + strings.ToUpper(t.task.Status)
desc := "Task ID " + strconv.Itoa(t.task.ID) + " (" + t.template.Name + ")" + " finished - " + strings.ToUpper(string(t.task.Status))
_, err := t.pool.store.CreateEvent(db.Event{
UserID: t.task.UserID,
@ -153,7 +152,7 @@ func (t *TaskRunner) prepareRun() {
defer func() {
log.Info("Stopped preparing TaskRunner " + strconv.Itoa(t.task.ID))
log.Info("Release resource locker with TaskRunner " + strconv.Itoa(t.task.ID))
resourceLocker <- &resourceLock{lock: false, holder: t}
t.pool.resourceLocker <- &resourceLock{lock: false, holder: t}
t.createTaskEvent()
}()
@ -167,13 +166,6 @@ func (t *TaskRunner) prepareRun() {
return
}
err = t.populateDetails()
if err != nil {
t.Log("Error: " + err.Error())
t.fail()
return
}
objType := db.EventTask
desc := "Task ID " + strconv.Itoa(t.task.ID) + " (" + t.template.Name + ")" + " is preparing"
_, err = t.pool.store.CreateEvent(db.Event{
@ -232,12 +224,6 @@ func (t *TaskRunner) prepareRun() {
return
}
if err := t.listPlaybookHosts(); err != nil {
t.Log("Listing playbook hosts failed: " + err.Error())
t.fail()
return
}
t.prepared = true
}
@ -245,7 +231,7 @@ func (t *TaskRunner) run() {
defer func() {
log.Info("Stopped running TaskRunner " + strconv.Itoa(t.task.ID))
log.Info("Release resource locker with TaskRunner " + strconv.Itoa(t.task.ID))
resourceLocker <- &resourceLock{lock: false, holder: t}
t.pool.resourceLocker <- &resourceLock{lock: false, holder: t}
now := time.Now()
t.task.End = &now
@ -533,25 +519,6 @@ func (t *TaskRunner) runGalaxy(args []string) error {
}.RunGalaxy(args)
}
func (t *TaskRunner) listPlaybookHosts() (err error) {
if util.Config.ConcurrencyMode == "project" {
return
}
args, err := t.getPlaybookArgs()
if err != nil {
return
}
t.hosts, err = lib.AnsiblePlaybook{
Logger: t,
TemplateID: t.template.ID,
Repository: t.repository,
}.GetHosts(args)
return
}
func (t *TaskRunner) runPlaybook() (err error) {
args, err := t.getPlaybookArgs()
if err != nil {

View File

@ -99,8 +99,7 @@ type ConfigType struct {
TelegramToken string `json:"telegram_token"`
// task concurrency
ConcurrencyMode string `json:"concurrency_mode"`
MaxParallelTasks int `json:"max_parallel_tasks"`
MaxParallelTasks int `json:"max_parallel_tasks"`
// configType field ordering with bools at end reduces struct size
// (maligned check)