Add per project rate limit

This commit is contained in:
simon987 2019-02-24 21:18:55 -05:00
parent 9acf6e27c1
commit 3415f95337
13 changed files with 347 additions and 267 deletions

View File

@ -9,15 +9,18 @@ import (
"github.com/simon987/task_tracker/config" "github.com/simon987/task_tracker/config"
"github.com/simon987/task_tracker/storage" "github.com/simon987/task_tracker/storage"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
"sync"
) )
type WebAPI struct { type WebAPI struct {
server *fasthttp.Server server *fasthttp.Server
router *fasthttprouter.Router router *fasthttprouter.Router
Database *storage.Database Database *storage.Database
SessionConfig sessions.Config SessionConfig sessions.Config
Session *sessions.Sessions Session *sessions.Sessions
Cron *cron.Cron Cron *cron.Cron
AssignLimiters sync.Map
SubmitLimiters sync.Map
} }
type RequestHandler func(*Request) type RequestHandler func(*Request)
@ -98,7 +101,6 @@ func New() *WebAPI {
api.router.POST("/task/submit", LogRequestMiddleware(api.SubmitTask)) api.router.POST("/task/submit", LogRequestMiddleware(api.SubmitTask))
api.router.GET("/task/get/:project", LogRequestMiddleware(api.GetTaskFromProject)) api.router.GET("/task/get/:project", LogRequestMiddleware(api.GetTaskFromProject))
api.router.GET("/task/get", LogRequestMiddleware(api.GetTask))
api.router.POST("/task/release", LogRequestMiddleware(api.ReleaseTask)) api.router.POST("/task/release", LogRequestMiddleware(api.ReleaseTask))
api.router.POST("/git/receivehook", LogRequestMiddleware(api.ReceiveGitWebHook)) api.router.POST("/git/receivehook", LogRequestMiddleware(api.ReceiveGitWebHook))

View File

@ -3,6 +3,7 @@ package api
import ( import (
"encoding/json" "encoding/json"
"github.com/simon987/task_tracker/storage" "github.com/simon987/task_tracker/storage"
"golang.org/x/time/rate"
) )
const ( const (
@ -12,9 +13,10 @@ const (
) )
type JsonResponse struct { type JsonResponse struct {
Ok bool `json:"ok"` Ok bool `json:"ok"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Content interface{} `json:"content,omitempty"` RateLimitDelay string `json:"rate_limit_delay,omitempty"`
Content interface{} `json:"content,omitempty"`
} }
type GitPayload struct { type GitPayload struct {
@ -115,15 +117,17 @@ type GetSnapshotsResponse struct {
} }
type CreateProjectRequest struct { type CreateProjectRequest struct {
Name string `json:"name"` Name string `json:"name"`
CloneUrl string `json:"clone_url"` CloneUrl string `json:"clone_url"`
GitRepo string `json:"git_repo"` GitRepo string `json:"git_repo"`
Version string `json:"version"` Version string `json:"version"`
Priority int64 `json:"priority"` Priority int64 `json:"priority"`
Motd string `json:"motd"` Motd string `json:"motd"`
Public bool `json:"public"` Public bool `json:"public"`
Hidden bool `json:"hidden"` Hidden bool `json:"hidden"`
Chain int64 `json:"chain"` Chain int64 `json:"chain"`
AssignRate rate.Limit `json:"assign_rate"`
SubmitRate rate.Limit `json:"submit_rate"`
} }
func (req *CreateProjectRequest) isValid() bool { func (req *CreateProjectRequest) isValid() bool {
@ -140,15 +144,17 @@ func (req *CreateProjectRequest) isValid() bool {
} }
type UpdateProjectRequest struct { type UpdateProjectRequest struct {
Name string `json:"name"` Name string `json:"name"`
CloneUrl string `json:"clone_url"` CloneUrl string `json:"clone_url"`
GitRepo string `json:"git_repo"` GitRepo string `json:"git_repo"`
Priority int64 `json:"priority"` Priority int64 `json:"priority"`
Motd string `json:"motd"` Motd string `json:"motd"`
Public bool `json:"public"` Public bool `json:"public"`
Hidden bool `json:"hidden"` Hidden bool `json:"hidden"`
Chain int64 `json:"chain"` Chain int64 `json:"chain"`
Paused bool `json:"paused"` Paused bool `json:"paused"`
AssignRate rate.Limit `json:"assign_rate"`
SubmitRate rate.Limit `json:"submit_rate"`
} }
func (req *UpdateProjectRequest) isValid() bool { func (req *UpdateProjectRequest) isValid() bool {

View File

@ -5,6 +5,7 @@ import (
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/simon987/task_tracker/storage" "github.com/simon987/task_tracker/storage"
"golang.org/x/time/rate"
"strconv" "strconv"
) )
@ -62,16 +63,25 @@ func (api *WebAPI) CreateProject(r *Request) {
}, 400) }, 400)
return return
} }
if createReq.AssignRate == 0 {
createReq.AssignRate = rate.Inf
}
if createReq.SubmitRate == 0 {
createReq.SubmitRate = rate.Inf
}
project := &storage.Project{ project := &storage.Project{
Name: createReq.Name, Name: createReq.Name,
Version: createReq.Version, Version: createReq.Version,
CloneUrl: createReq.CloneUrl, CloneUrl: createReq.CloneUrl,
GitRepo: createReq.GitRepo, GitRepo: createReq.GitRepo,
Priority: createReq.Priority, Priority: createReq.Priority,
Motd: createReq.Motd, Motd: createReq.Motd,
Public: createReq.Public, Public: createReq.Public,
Hidden: createReq.Hidden, Hidden: createReq.Hidden,
Chain: createReq.Chain, Chain: createReq.Chain,
AssignRate: createReq.AssignRate,
SubmitRate: createReq.SubmitRate,
} }
if !createReq.isValid() { if !createReq.isValid() {
@ -156,16 +166,18 @@ func (api *WebAPI) UpdateProject(r *Request) {
} }
project := &storage.Project{ project := &storage.Project{
Id: id, Id: id,
Name: updateReq.Name, Name: updateReq.Name,
CloneUrl: updateReq.CloneUrl, CloneUrl: updateReq.CloneUrl,
GitRepo: updateReq.GitRepo, GitRepo: updateReq.GitRepo,
Priority: updateReq.Priority, Priority: updateReq.Priority,
Motd: updateReq.Motd, Motd: updateReq.Motd,
Public: updateReq.Public, Public: updateReq.Public,
Hidden: updateReq.Hidden, Hidden: updateReq.Hidden,
Chain: updateReq.Chain, Chain: updateReq.Chain,
Paused: updateReq.Paused, Paused: updateReq.Paused,
AssignRate: updateReq.AssignRate,
SubmitRate: updateReq.SubmitRate,
} }
sess := api.Session.StartFasthttp(r.Ctx) sess := api.Session.StartFasthttp(r.Ctx)
manager := sess.Get("manager") manager := sess.Get("manager")

40
api/rate.go Normal file
View File

@ -0,0 +1,40 @@
package api
import (
"fmt"
"golang.org/x/time/rate"
"time"
)
func (api *WebAPI) ReserveSubmit(pid int64) *rate.Reservation {
limiter, ok := api.SubmitLimiters.Load(pid)
if !ok {
project := api.Database.GetProject(pid)
if project == nil {
return &rate.Reservation{}
}
limiter = rate.NewLimiter(project.SubmitRate, 1)
api.SubmitLimiters.Store(pid, limiter)
}
return limiter.(*rate.Limiter).ReserveN(time.Now(), 1)
}
func (api *WebAPI) ReserveAssign(pid int64) *rate.Reservation {
limiter, ok := api.AssignLimiters.Load(pid)
if !ok {
project := api.Database.GetProject(pid)
if project == nil {
return &rate.Reservation{}
}
limiter = rate.NewLimiter(project.AssignRate, 1)
api.AssignLimiters.Store(pid, limiter)
fmt.Printf("Create")
}
return limiter.(*rate.Limiter).ReserveN(time.Now(), 1)
}

View File

@ -11,6 +11,7 @@ import (
"github.com/dchest/siphash" "github.com/dchest/siphash"
"github.com/simon987/task_tracker/storage" "github.com/simon987/task_tracker/storage"
"strconv" "strconv"
"time"
) )
func (api *WebAPI) SubmitTask(r *Request) { func (api *WebAPI) SubmitTask(r *Request) {
@ -53,6 +54,17 @@ func (api *WebAPI) SubmitTask(r *Request) {
return return
} }
reservation := api.ReserveSubmit(createReq.Project)
delay := reservation.DelayFrom(time.Now()).Seconds()
if delay > 0 {
r.Json(JsonResponse{
Ok: false,
Message: "Too many requests",
RateLimitDelay: strconv.FormatFloat(delay, 'f', -1, 64),
}, 429)
return
}
if createReq.UniqueString != "" { if createReq.UniqueString != "" {
//TODO: Load key from config //TODO: Load key from config
createReq.Hash64 = int64(siphash.Hash(1, 2, []byte(createReq.UniqueString))) createReq.Hash64 = int64(siphash.Hash(1, 2, []byte(createReq.UniqueString)))
@ -65,6 +77,7 @@ func (api *WebAPI) SubmitTask(r *Request) {
Ok: false, Ok: false,
Message: err.Error(), Message: err.Error(),
}, 400) }, 400)
reservation.Cancel()
} else { } else {
r.OkJson(JsonResponse{ r.OkJson(JsonResponse{
Ok: true, Ok: true,
@ -84,47 +97,33 @@ func (api *WebAPI) GetTaskFromProject(r *Request) {
} }
project, err := strconv.ParseInt(r.Ctx.UserValue("project").(string), 10, 64) project, err := strconv.ParseInt(r.Ctx.UserValue("project").(string), 10, 64)
handleErr(err, r) if err != nil || project <= 0 {
task := api.Database.GetTaskFromProject(worker, project)
if task == nil {
r.OkJson(JsonResponse{
Ok: false,
Message: "No task available",
})
} else {
r.OkJson(JsonResponse{
Ok: true,
Content: GetTaskResponse{
Task: task,
},
})
}
}
func (api *WebAPI) GetTask(r *Request) {
worker, err := api.validateSignature(r)
if err != nil {
r.Json(JsonResponse{ r.Json(JsonResponse{
Ok: false, Ok: false,
Message: err.Error(), Message: "Invalid project id",
}, 403) }, 400)
return return
} }
task := api.Database.GetTask(worker) reservation := api.ReserveAssign(project)
if task == nil { delay := reservation.DelayFrom(time.Now()).Seconds()
if delay > 0 {
r.Json(JsonResponse{
Ok: false,
Message: "Too many requests",
RateLimitDelay: strconv.FormatFloat(delay, 'f', -1, 64),
}, 429)
return
}
task := api.Database.GetTaskFromProject(worker, project)
if task == nil {
r.OkJson(JsonResponse{ r.OkJson(JsonResponse{
Ok: false, Ok: false,
Message: "No task available", Message: "No task available",
}) })
reservation.CancelAt(time.Now())
} else { } else {
r.OkJson(JsonResponse{ r.OkJson(JsonResponse{
@ -134,8 +133,8 @@ func (api *WebAPI) GetTask(r *Request) {
}, },
}) })
} }
}
}
func (api WebAPI) validateSignature(r *Request) (*storage.Worker, error) { func (api WebAPI) validateSignature(r *Request) (*storage.Worker, error) {
widStr := string(r.Ctx.Request.Header.Peek("X-Worker-Id")) widStr := string(r.Ctx.Request.Header.Peek("X-Worker-Id"))

2
jenkins/Jenkinsfile vendored
View File

@ -68,7 +68,7 @@ pipeline {
node('master') { node('master') {
unstash 'webdist' unstash 'webdist'
unstash 'apidist' unstash 'apidist'
sshCommand remote: remote, command: "rm -rf tt_api config.yml schema.sql webroot deploy.sh" sshCommand remote: remote, command: "cd task_tracker && rm -rf tt_api config.yml schema.sql webroot deploy.sh"
sshPut remote: remote, from: 'tt_api', into: 'task_tracker/tt_api' sshPut remote: remote, from: 'tt_api', into: 'task_tracker/tt_api'
sshPut remote: remote, from: 'config.yml', into: 'task_tracker/config.yml' sshPut remote: remote, from: 'config.yml', into: 'task_tracker/config.yml'
sshPut remote: remote, from: 'schema.sql', into: 'task_tracker/schema.sql' sshPut remote: remote, from: 'schema.sql', into: 'task_tracker/schema.sql'

View File

@ -15,17 +15,20 @@ CREATE TABLE project
( (
id SERIAL PRIMARY KEY NOT NULL, id SERIAL PRIMARY KEY NOT NULL,
priority INTEGER DEFAULT 0 NOT NULL, priority INTEGER DEFAULT 0 NOT NULL,
closed_task_count INT DEFAULT 0 NOT NULL, closed_task_count INT DEFAULT 0 NOT NULL,
chain INT DEFAULT NULL REFERENCES project (id), chain INT DEFAULT NULL REFERENCES project (id),
public boolean NOT NULL, public boolean NOT NULL,
hidden boolean NOT NULL, hidden boolean NOT NULL,
paused boolean NOT NULL, paused boolean NOT NULL,
name TEXT UNIQUE NOT NULL, name TEXT UNIQUE NOT NULL,
clone_url TEXT NOT NULL, clone_url TEXT NOT NULL,
git_repo TEXT NOT NULL, git_repo TEXT NOT NULL,
version TEXT NOT NULL, version TEXT NOT NULL,
motd TEXT NOT NULL, motd TEXT NOT NULL,
secret TEXT NOT NULL DEFAULT '{}' secret TEXT NOT NULL DEFAULT '{}',
webhook_secret TEXT NOT NULL,
assign_rate DOUBLE PRECISION NOT NULL,
submit_rate DOUBLE PRECISION NOT NULL
); );
CREATE TABLE worker_access CREATE TABLE worker_access

View File

@ -3,21 +3,24 @@ package storage
import ( import (
"database/sql" "database/sql"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"golang.org/x/time/rate"
"strings" "strings"
) )
type Project struct { type Project struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Priority int64 `json:"priority"` Priority int64 `json:"priority"`
Name string `json:"name"` Name string `json:"name"`
CloneUrl string `json:"clone_url"` CloneUrl string `json:"clone_url"`
GitRepo string `json:"git_repo"` GitRepo string `json:"git_repo"`
Version string `json:"version"` Version string `json:"version"`
Motd string `json:"motd"` Motd string `json:"motd"`
Public bool `json:"public"` Public bool `json:"public"`
Hidden bool `json:"hidden"` Hidden bool `json:"hidden"`
Chain int64 `json:"chain"` Chain int64 `json:"chain"`
Paused bool `json:"paused"` Paused bool `json:"paused"`
AssignRate rate.Limit `json:"assign_rate"`
SubmitRate rate.Limit `json:"submit_rate"`
} }
type AssignedTasks struct { type AssignedTasks struct {
@ -29,10 +32,11 @@ func (database *Database) SaveProject(project *Project, webhookSecret string) (i
db := database.getDB() db := database.getDB()
row := db.QueryRow(`INSERT INTO project (name, git_repo, clone_url, version, priority, row := db.QueryRow(`INSERT INTO project (name, git_repo, clone_url, version, priority,
motd, public, hidden, chain, paused, webhook_secret) motd, public, hidden, chain, paused, webhook_secret, assign_rate, submit_rate)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NULLIF($9, 0),$10,$11) RETURNING id`, VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NULLIF($9, 0),$10,$11,$12,$13) RETURNING id`,
project.Name, project.GitRepo, project.CloneUrl, project.Version, project.Priority, project.Motd, project.Name, project.GitRepo, project.CloneUrl, project.Version, project.Priority, project.Motd,
project.Public, project.Hidden, project.Chain, project.Paused, webhookSecret) project.Public, project.Hidden, project.Chain, project.Paused, webhookSecret, project.AssignRate,
project.SubmitRate)
var id int64 var id int64
err := row.Scan(&id) err := row.Scan(&id)
@ -58,7 +62,7 @@ func (database *Database) GetProject(id int64) *Project {
db := database.getDB() db := database.getDB()
row := db.QueryRow(`SELECT id, priority, name, clone_url, git_repo, version, row := db.QueryRow(`SELECT id, priority, name, clone_url, git_repo, version,
motd, public, hidden, COALESCE(chain, 0), paused motd, public, hidden, COALESCE(chain, 0), paused, assign_rate, submit_rate
FROM project WHERE id=$1`, id) FROM project WHERE id=$1`, id)
project, err := scanProject(row) project, err := scanProject(row)
@ -81,7 +85,7 @@ func scanProject(row *sql.Row) (*Project, error) {
p := &Project{} p := &Project{}
err := row.Scan(&p.Id, &p.Priority, &p.Name, &p.CloneUrl, &p.GitRepo, &p.Version, err := row.Scan(&p.Id, &p.Priority, &p.Name, &p.CloneUrl, &p.GitRepo, &p.Version,
&p.Motd, &p.Public, &p.Hidden, &p.Chain, &p.Paused) &p.Motd, &p.Public, &p.Hidden, &p.Chain, &p.Paused, &p.AssignRate, &p.SubmitRate)
return p, err return p, err
} }
@ -90,7 +94,8 @@ func (database *Database) GetProjectWithRepoName(repoName string) *Project {
db := database.getDB() db := database.getDB()
row := db.QueryRow(`SELECT id, priority, name, clone_url, git_repo, version, row := db.QueryRow(`SELECT id, priority, name, clone_url, git_repo, version,
motd, public, hidden, COALESCE(chain, 0), paused FROM project WHERE LOWER(git_repo)=$1`, motd, public, hidden, COALESCE(chain, 0), paused, assign_rate, submit_rate
FROM project WHERE LOWER(git_repo)=$1`,
strings.ToLower(repoName)) strings.ToLower(repoName))
project, err := scanProject(row) project, err := scanProject(row)
@ -109,11 +114,13 @@ func (database *Database) UpdateProject(project *Project) error {
db := database.getDB() db := database.getDB()
res, err := db.Exec(`UPDATE project res, err := db.Exec(`UPDATE project
SET (priority, name, clone_url, git_repo, version, motd, public, hidden, chain, paused) = SET (priority, name, clone_url, git_repo, version, motd, public, hidden, chain, paused,
($1,$2,$3,$4,$5,$6,$7,$8,NULLIF($9, 0), $10) assign_rate, submit_rate) =
WHERE id=$11`, ($1,$2,$3,$4,$5,$6,$7,$8,NULLIF($9, 0), $10,$11,$12)
WHERE id=$13`,
project.Priority, project.Name, project.CloneUrl, project.GitRepo, project.Version, project.Motd, project.Priority, project.Name, project.CloneUrl, project.GitRepo, project.Version, project.Motd,
project.Public, project.Hidden, project.Chain, project.Paused, project.Id) project.Public, project.Hidden, project.Chain, project.Paused, project.AssignRate, project.SubmitRate,
project.Id)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,7 +1,6 @@
package storage package storage
import ( import (
"database/sql"
"errors" "errors"
"fmt" "fmt"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
@ -69,75 +68,6 @@ func (database *Database) SaveTask(task *Task, project int64, hash64 int64, wid
return nil return nil
} }
func (database *Database) GetTask(worker *Worker) *Task {
db := database.getDB()
row := db.QueryRow(`
UPDATE task
SET assignee=$1, assign_time=extract(epoch from now() at time zone 'utc')
WHERE id IN
(
SELECT task.id
FROM task
INNER JOIN project project on task.project = project.id
LEFT JOIN worker_verifies_task wvt on task.id = wvt.task AND wvt.worker=$1
WHERE NOT project.paused AND assignee IS NULL AND task.status=1
AND (project.public OR (
SELECT a.role_assign AND not a.request FROM worker_access a WHERE a.worker=$1 AND a.project=project.id
))
AND wvt.task IS NULL
ORDER BY project.priority DESC, task.priority DESC
LIMIT 1
)
RETURNING id`, worker.Id)
var id int64
err := row.Scan(&id)
if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{
"worker": worker,
}).Trace("No task available")
return nil
}
logrus.WithFields(logrus.Fields{
"id": id,
"worker": worker,
}).Trace("Database.getTask UPDATE task")
task := getTaskById(id, db)
return task
}
func getTaskById(id int64, db *sql.DB) *Task {
row := db.QueryRow(`
SELECT task.id, task.priority, task.project, assignee, retries, max_retries,
status, recipe, max_assign_time, assign_time, verification_count, project.priority, project.name,
project.clone_url, project.git_repo, project.version, project.motd, project.public, COALESCE(project.chain,0) FROM task
INNER JOIN project project ON task.project = project.id
WHERE task.id=$1`, id)
project := &Project{}
task := &Task{}
task.Project = project
err := row.Scan(&task.Id, &task.Priority, &project.Id, &task.Assignee,
&task.Retries, &task.MaxRetries, &task.Status, &task.Recipe, &task.MaxAssignTime,
&task.AssignTime, &task.VerificationCount, &project.Priority, &project.Name,
&project.CloneUrl, &project.GitRepo, &project.Version, &project.Motd, &project.Public,
&project.Chain)
handleErr(err)
logrus.WithFields(logrus.Fields{
"id": id,
"task": task,
}).Trace("Database.getTaskById SELECT task")
return task
}
func (database Database) ReleaseTask(id int64, workerId int64, result TaskResult, verification int64) bool { func (database Database) ReleaseTask(id int64, workerId int64, result TaskResult, verification int64) bool {
db := database.getDB() db := database.getDB()
@ -205,12 +135,24 @@ func (database *Database) GetTaskFromProject(worker *Worker, projectId int64) *T
return nil return nil
} }
logrus.WithFields(logrus.Fields{ row = db.QueryRow(`
"id": id, SELECT task.id, task.priority, task.project, assignee, retries, max_retries,
"worker": worker, status, recipe, max_assign_time, assign_time, verification_count, project.priority, project.name,
}).Trace("Database.getTask UPDATE task") project.clone_url, project.git_repo, project.version, project.motd, project.public, COALESCE(project.chain,0),
project.assign_rate, project.submit_rate
FROM task
INNER JOIN project project ON task.project = project.id
WHERE task.id=$1`, id)
project := &Project{}
task := &Task{}
task.Project = project
task := getTaskById(id, db) err = row.Scan(&task.Id, &task.Priority, &project.Id, &task.Assignee,
&task.Retries, &task.MaxRetries, &task.Status, &task.Recipe, &task.MaxAssignTime,
&task.AssignTime, &task.VerificationCount, &project.Priority, &project.Name,
&project.CloneUrl, &project.GitRepo, &project.Version, &project.Motd, &project.Public,
&project.Chain, &project.AssignRate, &project.SubmitRate)
handleErr(err)
return task return task
} }

View File

@ -13,14 +13,16 @@ import (
func TestCreateGetProject(t *testing.T) { func TestCreateGetProject(t *testing.T) {
resp := createProjectAsAdmin(api.CreateProjectRequest{ resp := createProjectAsAdmin(api.CreateProjectRequest{
Name: "Test name", Name: "Test name",
CloneUrl: "http://github.com/test/test", CloneUrl: "http://github.com/test/test",
GitRepo: "drone/webhooktest", GitRepo: "drone/webhooktest",
Version: "Test Version", Version: "Test Version",
Priority: 123, Priority: 123,
Motd: "motd", Motd: "motd",
Public: true, Public: true,
Hidden: false, Hidden: false,
AssignRate: 10.0,
SubmitRate: 20.0,
}) })
id := resp.Content.Id id := resp.Content.Id
@ -64,6 +66,12 @@ func TestCreateGetProject(t *testing.T) {
if getResp.Project.Hidden != false { if getResp.Project.Hidden != false {
t.Error() t.Error()
} }
if getResp.Project.SubmitRate != 20.0 {
t.Error()
}
if getResp.Project.AssignRate != 10.0 {
t.Error()
}
} }
func TestCreateProjectInvalid(t *testing.T) { func TestCreateProjectInvalid(t *testing.T) {
@ -107,24 +115,28 @@ func TestGetProjectNotFound(t *testing.T) {
func TestUpdateProjectValid(t *testing.T) { func TestUpdateProjectValid(t *testing.T) {
pid := createProjectAsAdmin(api.CreateProjectRequest{ pid := createProjectAsAdmin(api.CreateProjectRequest{
Public: true, Public: true,
Version: "versionA", Version: "versionA",
Motd: "MotdA", Motd: "MotdA",
Name: "NameA", Name: "NameA",
CloneUrl: "CloneUrlA", CloneUrl: "CloneUrlA",
GitRepo: "GitRepoA", GitRepo: "GitRepoA",
Priority: 1, Priority: 1,
AssignRate: 3,
SubmitRate: 3,
}).Content.Id }).Content.Id
updateResp := updateProject(api.UpdateProjectRequest{ updateResp := updateProject(api.UpdateProjectRequest{
Priority: 2, Priority: 2,
GitRepo: "GitRepoB", GitRepo: "GitRepoB",
CloneUrl: "CloneUrlB", CloneUrl: "CloneUrlB",
Name: "NameB", Name: "NameB",
Motd: "MotdB", Motd: "MotdB",
Public: false, Public: false,
Hidden: true, Hidden: true,
Paused: true, Paused: true,
AssignRate: 1,
SubmitRate: 2,
}, pid, testAdminCtx) }, pid, testAdminCtx)
if updateResp.Ok != true { if updateResp.Ok != true {
@ -154,6 +166,12 @@ func TestUpdateProjectValid(t *testing.T) {
if proj.Project.Paused != true { if proj.Project.Paused != true {
t.Error() t.Error()
} }
if proj.Project.AssignRate != 1 {
t.Error()
}
if proj.Project.SubmitRate != 2 {
t.Error()
}
} }
func TestUpdateProjectInvalid(t *testing.T) { func TestUpdateProjectInvalid(t *testing.T) {

80
test/api_rate_test.go Normal file
View File

@ -0,0 +1,80 @@
package test
import (
"fmt"
"github.com/simon987/task_tracker/api"
"testing"
)
func TestAssignRateLimit(t *testing.T) {
project := createProjectAsAdmin(api.CreateProjectRequest{
SubmitRate: 2,
AssignRate: 2,
Name: "testassignratelimit",
GitRepo: "testassignratelimit",
CloneUrl: "testassignratelimit",
}).Content.Id
w := genWid()
requestAccess(api.CreateWorkerAccessRequest{
Project: project,
Submit: true,
Assign: true,
}, w)
acceptAccessRequest(project, w.Id, testAdminCtx)
for i := 0; i < 3; i++ {
createTask(api.SubmitTaskRequest{
Project: project,
Recipe: fmt.Sprintf("%d", i),
}, w)
}
var lastResp TaskAR
for i := 0; i < 3; i++ {
lastResp = getTaskFromProject(project, w)
}
if lastResp.Ok != false {
t.Error()
}
if len(lastResp.Message) <= 0 {
t.Error()
}
}
func TestSubmitRateLimit(t *testing.T) {
project := createProjectAsAdmin(api.CreateProjectRequest{
SubmitRate: 2,
AssignRate: 2,
Name: "testsubmitratlimit",
GitRepo: "testsubmitratelimit",
CloneUrl: "testsubmitratelimit",
}).Content.Id
w := genWid()
requestAccess(api.CreateWorkerAccessRequest{
Project: project,
Submit: true,
Assign: true,
}, w)
acceptAccessRequest(project, w.Id, testAdminCtx)
var lastResp api.JsonResponse
for i := 0; i < 2; i++ {
lastResp = createTask(api.SubmitTaskRequest{
Project: project,
Recipe: fmt.Sprintf("%d", i),
}, w)
}
if lastResp.Ok != false {
t.Error()
}
if len(lastResp.Message) <= 0 {
t.Error()
}
}

View File

@ -57,7 +57,7 @@ func TestCreateTaskInvalidProject(t *testing.T) {
func TestGetTaskInvalidWid(t *testing.T) { func TestGetTaskInvalidWid(t *testing.T) {
resp := getTask(nil) resp := getTaskFromProject(testProject, genWid())
if resp.Ok != false { if resp.Ok != false {
t.Error() t.Error()
@ -70,7 +70,7 @@ func TestGetTaskInvalidWid(t *testing.T) {
func TestGetTaskInvalidWorker(t *testing.T) { func TestGetTaskInvalidWorker(t *testing.T) {
resp := getTask(&storage.Worker{ resp := getTaskFromProject(testProject, &storage.Worker{
Id: -1, Id: -1,
}) })
@ -138,12 +138,14 @@ func TestCreateTaskInvalidRecipe(t *testing.T) {
func TestCreateGetTask(t *testing.T) { func TestCreateGetTask(t *testing.T) {
pid := createProjectAsAdmin(api.CreateProjectRequest{ pid := createProjectAsAdmin(api.CreateProjectRequest{
Name: "My project", Name: "My project",
Version: "1.0", Version: "1.0",
CloneUrl: "http://github.com/test/test", CloneUrl: "http://github.com/test/test",
GitRepo: "myrepo", GitRepo: "myrepo",
Priority: 999, Priority: 999,
Public: true, Public: true,
AssignRate: 2,
SubmitRate: 2,
}).Content.Id }).Content.Id
worker := genWid() worker := genWid()
@ -197,6 +199,12 @@ func TestCreateGetTask(t *testing.T) {
if taskResp.Task.Project.Public != true { if taskResp.Task.Project.Public != true {
t.Error() t.Error()
} }
if taskResp.Task.Project.AssignRate == 1 {
t.Error()
}
if taskResp.Task.Project.SubmitRate != 2 {
t.Error()
}
} }
func createTasks(prefix string) (int64, int64) { func createTasks(prefix string) (int64, int64) {
@ -279,36 +287,6 @@ func TestTaskProjectPriority(t *testing.T) {
} }
} }
func TestTaskPriority(t *testing.T) {
wid := genWid()
// Clean other tasks
for i := 0; i < 20; i++ {
getTask(wid)
}
createTasks("")
t1 := getTask(wid).Content
t2 := getTask(wid).Content
t3 := getTask(wid).Content
t4 := getTask(wid).Content
if t1.Task.Recipe != "high2" {
t.Error()
}
if t2.Task.Recipe != "high1" {
t.Error()
}
if t3.Task.Recipe != "low2" {
t.Error()
}
if t4.Task.Recipe != "low1" {
t.Error()
}
}
func TestTaskNoAccess(t *testing.T) { func TestTaskNoAccess(t *testing.T) {
worker := genWid() worker := genWid()
@ -400,15 +378,6 @@ func TestTaskHasAccess(t *testing.T) {
} }
} }
func TestNoMoreTasks(t *testing.T) {
worker := genWid()
for i := 0; i < 15; i++ {
getTask(worker)
}
}
func TestReleaseTaskSuccess(t *testing.T) { func TestReleaseTaskSuccess(t *testing.T) {
worker := genWid() worker := genWid()

View File

@ -16,17 +16,19 @@ CREATE TABLE project
id SERIAL PRIMARY KEY NOT NULL, id SERIAL PRIMARY KEY NOT NULL,
priority INTEGER DEFAULT 0 NOT NULL, priority INTEGER DEFAULT 0 NOT NULL,
closed_task_count INT DEFAULT 0 NOT NULL, closed_task_count INT DEFAULT 0 NOT NULL,
chain INT DEFAULT NULL REFERENCES project (id), chain INT DEFAULT NULL REFERENCES project (id),
public boolean NOT NULL, public boolean NOT NULL,
hidden boolean NOT NULL, hidden boolean NOT NULL,
paused boolean NOT NULL, paused boolean NOT NULL,
name TEXT UNIQUE NOT NULL, name TEXT UNIQUE NOT NULL,
clone_url TEXT NOT NULL, clone_url TEXT NOT NULL,
git_repo TEXT NOT NULL, git_repo TEXT NOT NULL,
version TEXT NOT NULL, version TEXT NOT NULL,
motd TEXT NOT NULL, motd TEXT NOT NULL,
secret TEXT NOT NULL DEFAULT '{}', secret TEXT NOT NULL DEFAULT '{}',
webhook_secret TEXT NOT NULL webhook_secret TEXT NOT NULL,
assign_rate DOUBLE PRECISION NOT NULL,
submit_rate DOUBLE PRECISION NOT NULL
); );
CREATE TABLE worker_access CREATE TABLE worker_access