Performance patch, version bump

This commit is contained in:
simon 2019-09-21 14:32:18 -04:00
parent 77b4da0653
commit 3123abceb6
34 changed files with 362 additions and 257 deletions

View File

@ -5,7 +5,6 @@ import (
"errors" "errors"
"github.com/simon987/task_tracker/config" "github.com/simon987/task_tracker/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/valyala/fasthttp"
"os" "os"
"time" "time"
) )
@ -16,22 +15,6 @@ func (e *LogRequest) Time() time.Time {
return t return t
} }
func LogRequestMiddleware(h RequestHandler) fasthttp.RequestHandler {
return fasthttp.RequestHandler(func(ctx *fasthttp.RequestCtx) {
ctx.Response.Header.Add("Access-Control-Allow-Headers", "Content-Type")
ctx.Response.Header.Add("Access-Control-Allow-Methods", "GET, POST, OPTION")
ctx.Response.Header.Add("Access-Control-Allow-Origin", "*")
logrus.WithFields(logrus.Fields{
"path": string(ctx.Path()),
"header": ctx.Request.Header.String(),
}).Trace(string(ctx.Method()))
h(&Request{Ctx: ctx})
})
}
func (api *WebAPI) SetupLogger() { func (api *WebAPI) SetupLogger() {
writer, err := os.OpenFile("log.txt", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) writer, err := os.OpenFile("log.txt", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil { if err != nil {

View File

@ -27,7 +27,13 @@ type RequestHandler func(*Request)
var info = Info{ var info = Info{
Name: "task_tracker", Name: "task_tracker",
Version: "1.0", Version: "1.1",
}
func Middleware(h RequestHandler) fasthttp.RequestHandler {
return fasthttp.RequestHandler(func(ctx *fasthttp.RequestCtx) {
h(&Request{Ctx: ctx})
})
} }
func Index(r *Request) { func Index(r *Request) {
@ -77,56 +83,56 @@ func New() *WebAPI {
Name: info.Name, Name: info.Name,
} }
api.router.GET("/", LogRequestMiddleware(Index)) api.router.GET("/", Middleware(Index))
api.router.POST("/log/trace", LogRequestMiddleware(api.LogTrace)) api.router.POST("/log/trace", Middleware(api.LogTrace))
api.router.POST("/log/info", LogRequestMiddleware(api.LogInfo)) api.router.POST("/log/info", Middleware(api.LogInfo))
api.router.POST("/log/warn", LogRequestMiddleware(api.LogWarn)) api.router.POST("/log/warn", Middleware(api.LogWarn))
api.router.POST("/log/error", LogRequestMiddleware(api.LogError)) api.router.POST("/log/error", Middleware(api.LogError))
api.router.POST("/worker/create", LogRequestMiddleware(api.CreateWorker)) api.router.POST("/worker/create", Middleware(api.CreateWorker))
api.router.POST("/worker/update", LogRequestMiddleware(api.UpdateWorker)) api.router.POST("/worker/update", Middleware(api.UpdateWorker))
api.router.POST("/worker/set_paused", LogRequestMiddleware(api.WorkerSetPaused)) api.router.POST("/worker/set_paused", Middleware(api.WorkerSetPaused))
api.router.GET("/worker/get/:id", LogRequestMiddleware(api.GetWorker)) api.router.GET("/worker/get/:id", Middleware(api.GetWorker))
api.router.GET("/worker/stats", LogRequestMiddleware(api.GetAllWorkerStats)) api.router.GET("/worker/stats", Middleware(api.GetAllWorkerStats))
api.router.POST("/project/create", LogRequestMiddleware(api.CreateProject)) api.router.POST("/project/create", Middleware(api.CreateProject))
api.router.GET("/project/get/:id", LogRequestMiddleware(api.GetProject)) api.router.GET("/project/get/:id", Middleware(api.GetProject))
api.router.POST("/project/update/:id", LogRequestMiddleware(api.UpdateProject)) api.router.POST("/project/update/:id", Middleware(api.UpdateProject))
api.router.GET("/project/list", LogRequestMiddleware(api.GetProjectList)) api.router.GET("/project/list", Middleware(api.GetProjectList))
api.router.GET("/project/monitoring-between/:id", LogRequestMiddleware(api.GetSnapshotsWithinRange)) api.router.GET("/project/monitoring-between/:id", Middleware(api.GetSnapshotsWithinRange))
api.router.GET("/project/monitoring/:id", LogRequestMiddleware(api.GetNSnapshots)) api.router.GET("/project/monitoring/:id", Middleware(api.GetNSnapshots))
api.router.GET("/project/assignees/:id", LogRequestMiddleware(api.GetAssigneeStatsForProject)) api.router.GET("/project/assignees/:id", Middleware(api.GetAssigneeStatsForProject))
api.router.GET("/project/access_list/:id", LogRequestMiddleware(api.GetWorkerAccessListForProject)) api.router.GET("/project/access_list/:id", Middleware(api.GetWorkerAccessListForProject))
api.router.POST("/project/request_access", LogRequestMiddleware(api.CreateWorkerAccess)) api.router.POST("/project/request_access", Middleware(api.CreateWorkerAccess))
api.router.POST("/project/accept_request/:id/:wid", LogRequestMiddleware(api.AcceptAccessRequest)) api.router.POST("/project/accept_request/:id/:wid", Middleware(api.AcceptAccessRequest))
api.router.POST("/project/reject_request/:id/:wid", LogRequestMiddleware(api.RejectAccessRequest)) api.router.POST("/project/reject_request/:id/:wid", Middleware(api.RejectAccessRequest))
api.router.GET("/project/secret/:id", LogRequestMiddleware(api.GetSecret)) api.router.GET("/project/secret/:id", Middleware(api.GetSecret))
api.router.POST("/project/secret/:id", LogRequestMiddleware(api.SetSecret)) api.router.POST("/project/secret/:id", Middleware(api.SetSecret))
api.router.GET("/project/webhook_secret/:id", LogRequestMiddleware(api.GetWebhookSecret)) api.router.GET("/project/webhook_secret/:id", Middleware(api.GetWebhookSecret))
api.router.POST("/project/webhook_secret/:id", LogRequestMiddleware(api.SetWebhookSecret)) api.router.POST("/project/webhook_secret/:id", Middleware(api.SetWebhookSecret))
api.router.POST("/project/reset_failed_tasks/:id", LogRequestMiddleware(api.ResetFailedTasks)) api.router.POST("/project/reset_failed_tasks/:id", Middleware(api.ResetFailedTasks))
api.router.POST("/project/hard_reset/:id", LogRequestMiddleware(api.HardReset)) api.router.POST("/project/hard_reset/:id", Middleware(api.HardReset))
api.router.POST("/project/reclaim_assigned_tasks/:id", LogRequestMiddleware(api.ReclaimAssignedTasks)) api.router.POST("/project/reclaim_assigned_tasks/:id", Middleware(api.ReclaimAssignedTasks))
api.router.POST("/task/submit", LogRequestMiddleware(api.SubmitTask)) api.router.POST("/task/submit", Middleware(api.SubmitTask))
api.router.POST("/task/bulk_submit", LogRequestMiddleware(api.BulkSubmitTask)) api.router.POST("/task/bulk_submit", Middleware(api.BulkSubmitTask))
api.router.GET("/task/get/:project", LogRequestMiddleware(api.GetTaskFromProject)) api.router.GET("/task/get/:project", Middleware(api.GetTaskFromProject))
api.router.POST("/task/release", LogRequestMiddleware(api.ReleaseTask)) api.router.POST("/task/release", Middleware(api.ReleaseTask))
api.router.POST("/git/receivehook", LogRequestMiddleware(api.ReceiveGitWebHook)) api.router.POST("/git/receivehook", Middleware(api.ReceiveGitWebHook))
api.router.POST("/logs", LogRequestMiddleware(api.GetLog)) api.router.POST("/logs", Middleware(api.GetLog))
api.router.POST("/register", LogRequestMiddleware(api.Register)) api.router.POST("/register", Middleware(api.Register))
api.router.POST("/login", LogRequestMiddleware(api.Login)) api.router.POST("/login", Middleware(api.Login))
api.router.GET("/logout", LogRequestMiddleware(api.Logout)) api.router.GET("/logout", Middleware(api.Logout))
api.router.GET("/account", LogRequestMiddleware(api.GetAccountDetails)) api.router.GET("/account", Middleware(api.GetAccountDetails))
api.router.GET("/manager/list", LogRequestMiddleware(api.GetManagerList)) api.router.GET("/manager/list", Middleware(api.GetManagerList))
api.router.GET("/manager/list_for_project/:id", LogRequestMiddleware(api.GetManagerListWithRoleOn)) api.router.GET("/manager/list_for_project/:id", Middleware(api.GetManagerListWithRoleOn))
api.router.GET("/manager/promote/:id", LogRequestMiddleware(api.PromoteManager)) api.router.GET("/manager/promote/:id", Middleware(api.PromoteManager))
api.router.GET("/manager/demote/:id", LogRequestMiddleware(api.DemoteManager)) api.router.GET("/manager/demote/:id", Middleware(api.DemoteManager))
api.router.POST("/manager/set_role_for_project/:id", LogRequestMiddleware(api.SetManagerRoleOnProject)) api.router.POST("/manager/set_role_for_project/:id", Middleware(api.SetManagerRoleOnProject))
api.router.NotFound = func(ctx *fasthttp.RequestCtx) { api.router.NotFound = func(ctx *fasthttp.RequestCtx) {

View File

@ -196,13 +196,13 @@ type GetWorkerAccessListForProjectResponse struct {
type SubmitTaskRequest struct { type SubmitTaskRequest struct {
Project int64 `json:"project"` Project int64 `json:"project"`
MaxRetries int64 `json:"max_retries"` MaxRetries int16 `json:"max_retries"`
Recipe string `json:"recipe"` Recipe string `json:"recipe"`
Priority int64 `json:"priority"` Priority int16 `json:"priority"`
MaxAssignTime int64 `json:"max_assign_time"` MaxAssignTime int64 `json:"max_assign_time"`
Hash64 int64 `json:"hash_u64"` Hash64 int64 `json:"hash_u64"`
UniqueString string `json:"unique_string"` UniqueString string `json:"unique_string"`
VerificationCount int64 `json:"verification_count"` VerificationCount int16 `json:"verification_count"`
} }
func (req *SubmitTaskRequest) IsValid() bool { func (req *SubmitTaskRequest) IsValid() bool {
@ -212,7 +212,7 @@ func (req *SubmitTaskRequest) IsValid() bool {
if len(req.Recipe) <= 0 { if len(req.Recipe) <= 0 {
return false return false
} }
if req.Hash64 != 0 && req.UniqueString != "" { if req.Hash64 != 0 && len(req.UniqueString) != 0 {
return false return false
} }
if req.Project == 0 { if req.Project == 0 {

View File

@ -165,6 +165,12 @@ func (api *WebAPI) UpdateProject(r *Request) {
return return
} }
if updateReq.AssignRate == 0 {
updateReq.AssignRate = rate.Inf
}
if updateReq.SubmitRate == 0 {
updateReq.SubmitRate = rate.Inf
}
project := &storage.Project{ project := &storage.Project{
Id: id, Id: id,
Name: updateReq.Name, Name: updateReq.Name,

View File

@ -33,6 +33,10 @@ func (api *WebAPI) SubmitTask(r *Request) {
return return
} }
if createReq.VerificationCount == 0 {
createReq.VerificationCount = 1
}
if !createReq.IsValid() { if !createReq.IsValid() {
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"req": createReq, "req": createReq,
@ -46,7 +50,7 @@ func (api *WebAPI) SubmitTask(r *Request) {
task := &storage.Task{ task := &storage.Task{
MaxRetries: createReq.MaxRetries, MaxRetries: createReq.MaxRetries,
Recipe: createReq.Recipe, Recipe: string(createReq.Recipe),
Priority: createReq.Priority, Priority: createReq.Priority,
AssignTime: 0, AssignTime: 0,
MaxAssignTime: createReq.MaxAssignTime, MaxAssignTime: createReq.MaxAssignTime,
@ -72,7 +76,7 @@ func (api *WebAPI) SubmitTask(r *Request) {
return return
} }
if createReq.UniqueString != "" { if len(createReq.UniqueString) != 0 {
createReq.Hash64 = int64(siphash.Hash(1, 2, []byte(createReq.UniqueString))) createReq.Hash64 = int64(siphash.Hash(1, 2, []byte(createReq.UniqueString)))
} }
@ -135,14 +139,17 @@ func (api *WebAPI) BulkSubmitTask(r *Request) {
return return
} }
if req.UniqueString != "" { if len(req.UniqueString) != 0 {
req.Hash64 = int64(siphash.Hash(1, 2, []byte(req.UniqueString))) req.Hash64 = int64(siphash.Hash(1, 2, []byte(req.UniqueString)))
} }
if req.VerificationCount == 0 {
req.VerificationCount = 1
}
saveRequests[i] = storage.SaveTaskRequest{ saveRequests[i] = storage.SaveTaskRequest{
Task: &storage.Task{ Task: &storage.Task{
MaxRetries: req.MaxRetries, MaxRetries: req.MaxRetries,
Recipe: req.Recipe, Recipe: string(req.Recipe),
Priority: req.Priority, Priority: req.Priority,
AssignTime: 0, AssignTime: 0,
MaxAssignTime: req.MaxAssignTime, MaxAssignTime: req.MaxAssignTime,
@ -184,6 +191,8 @@ func (api *WebAPI) BulkSubmitTask(r *Request) {
return return
} }
logrus.Info(saveErrors)
r.OkJson(JsonResponse{ r.OkJson(JsonResponse{
Ok: true, Ok: true,
}) })
@ -263,7 +272,7 @@ func (api *WebAPI) validateSecret(r *Request) (*storage.Worker, error) {
if widStr == "" { if widStr == "" {
return nil, errors.New("worker id not specified") return nil, errors.New("worker id not specified")
} }
if bytes.Equal(secretHeader, []byte("")) { if len(secretHeader) == 0 {
return nil, errors.New("secret is not specified") return nil, errors.New("secret is not specified")
} }
@ -290,12 +299,6 @@ func (api *WebAPI) validateSecret(r *Request) (*storage.Worker, error) {
secretLen, _ := base64.StdEncoding.Decode(secret, secretHeader) secretLen, _ := base64.StdEncoding.Decode(secret, secretHeader)
matches := bytes.Equal(worker.Secret, secret[:secretLen]) matches := bytes.Equal(worker.Secret, secret[:secretLen])
logrus.WithFields(logrus.Fields{
"expected": string(worker.Secret),
"header": string(secretHeader),
"matches": matches,
}).Trace("Validating Worker secret")
if !matches { if !matches {
return nil, errors.New("invalid secret") return nil, errors.New("invalid secret")
} }

View File

@ -21,7 +21,19 @@ type AssignTaskResponse struct {
Message string `json:"message"` Message string `json:"message"`
RateLimitDelay float64 `json:"rate_limit_delay,omitempty"` RateLimitDelay float64 `json:"rate_limit_delay,omitempty"`
Content struct { Content struct {
Task *storage.Task `json:"task"` Task *struct {
Id int64 `json:"id"`
Priority int16 `json:"priority"`
Project *storage.Project `json:"project"`
Assignee int64 `json:"assignee"`
Retries int16 `json:"retries"`
MaxRetries int64 `json:"max_retries"`
Status storage.TaskStatus `json:"status"`
Recipe string `json:"recipe"`
MaxAssignTime int64 `json:"max_assign_time"`
AssignTime int64 `json:"assign_time"`
VerificationCount int16 `json:"verification_count"`
} `json:"task"`
} `json:"content"` } `json:"content"`
} }

0
config.yml Executable file → Normal file
View File

1
jenkins/Jenkinsfile vendored
View File

@ -7,6 +7,7 @@ remote.knownHosts = '/var/lib/jenkins/.ssh/known_hosts'
remote.allowAnyHosts = true remote.allowAnyHosts = true
remote.retryCount = 3 remote.retryCount = 3
remote.retryWaitSec = 3 remote.retryWaitSec = 3
remote.port = 2299
logLevel = 'FINER' logLevel = 'FINER'
pipeline { pipeline {

0
jenkins/deploy.sh Executable file → Normal file
View File

View File

@ -3,18 +3,10 @@ package main
import ( import (
"github.com/simon987/task_tracker/api" "github.com/simon987/task_tracker/api"
"github.com/simon987/task_tracker/config" "github.com/simon987/task_tracker/config"
//"github.com/simon987/task_tracker/storage"
"math/rand" "math/rand"
"time" "time"
) )
func tmpDebugSetup() {
//db := storage.Database{}
//db.Reset()
}
func main() { func main() {
rand.Seed(time.Now().UTC().UnixNano()) rand.Seed(time.Now().UTC().UnixNano())
@ -22,6 +14,5 @@ func main() {
webApi := api.New() webApi := api.New()
webApi.SetupLogger() webApi.SetupLogger()
tmpDebugSetup()
webApi.Run() webApi.Run()
} }

24
schema.sql Executable file → Normal file
View File

@ -41,6 +41,8 @@ CREATE TABLE worker_access
request boolean, request boolean,
primary key (worker, project) primary key (worker, project)
); );
CREATE INDEX worker_index ON worker_access (worker);
CREATE INDEX project_index ON worker_access (project);
CREATE TABLE task CREATE TABLE task
( (
@ -50,22 +52,28 @@ CREATE TABLE task
assignee INTEGER REFERENCES worker (id), assignee INTEGER REFERENCES worker (id),
max_assign_time INTEGER DEFAULT 0, max_assign_time INTEGER DEFAULT 0,
assign_time INTEGER DEFAULT NULL, assign_time INTEGER DEFAULT NULL,
verification_count INTEGER DEFAULT 0, verification_count SMALLINT DEFAULT 0,
priority SMALLINT DEFAULT 0, priority SMALLINT DEFAULT 0,
retries SMALLINT DEFAULT 0, retries SMALLINT DEFAULT 0,
max_retries SMALLINT, max_retries SMALLINT,
status SMALLINT DEFAULT 1, status SMALLINT DEFAULT 1,
recipe TEXT, recipe TEXT
UNIQUE (project, hash64)
); );
CREATE INDEX priority_desc_index ON task (priority DESC);
CREATE INDEX assignee_index ON task (assignee);
CREATE INDEX verifcnt_index ON task (verification_count);
CREATE UNIQUE INDEX project_hash_unique ON task (project, hash64);
CREATE TABLE worker_verifies_task CREATE TABLE worker_verifies_task
( (
verification_hash BIGINT NOT NULL, verification_hash BIGINT NOT NULL,
task BIGINT REFERENCES task (id) ON DELETE CASCADE NOT NULL, task INT REFERENCES task (id) ON DELETE CASCADE NOT NULL,
worker INT REFERENCES worker (id) NOT NULL worker INT REFERENCES worker (id) NOT NULL
); );
CREATE INDEX task_index ON worker_verifies_task (task);
CREATE TABLE log_entry CREATE TABLE log_entry
( (
level INTEGER NOT NULL, level INTEGER NOT NULL,
@ -150,7 +158,7 @@ $$
DECLARE DECLARE
res INT = NULL; res INT = NULL;
BEGIN BEGIN
DELETE FROM task WHERE id = tid AND assignee = wid AND verification_count < 2 RETURNING project INTO res; DELETE FROM task WHERE id = tid AND assignee = wid AND verification_count = 1 RETURNING project INTO res;
IF res IS NULL THEN IF res IS NULL THEN
INSERT INTO worker_verifies_task (worker, verification_hash, task) INSERT INTO worker_verifies_task (worker, verification_hash, task)
@ -171,7 +179,7 @@ BEGIN
LIMIT 1) >= task.verification_count RETURNING task.id INTO res; LIMIT 1) >= task.verification_count RETURNING task.id INTO res;
IF res IS NULL THEN IF res IS NULL THEN
UPDATE task SET assignee= NULL WHERE id = tid AND assignee = wid; UPDATE task SET assignee=NULL WHERE id = tid AND assignee = wid;
end if; end if;
end if; end if;

View File

@ -14,7 +14,13 @@ type Database struct {
db *sql.DB db *sql.DB
saveTaskStmt *sql.Stmt saveTaskStmt *sql.Stmt
workerCache map[int64]*Worker workerCache map[int64]*Worker
projectCache map[int64]*Project
// [worker][project]
submitAccessCache map[int64]map[int64]bool
assignAccessCache map[int64]map[int64]bool
assignMutex *sync.Mutex assignMutex *sync.Mutex
} }
@ -22,6 +28,9 @@ func New() *Database {
d := Database{} d := Database{}
d.workerCache = make(map[int64]*Worker) d.workerCache = make(map[int64]*Worker)
d.projectCache = make(map[int64]*Project)
d.assignAccessCache = make(map[int64]map[int64]bool)
d.submitAccessCache = make(map[int64]map[int64]bool)
d.assignMutex = &sync.Mutex{} d.assignMutex = &sync.Mutex{}
d.init() d.init()
@ -68,10 +77,12 @@ func (database *Database) getDB() *sql.DB {
db.SetMaxOpenConns(50) db.SetMaxOpenConns(50)
database.db = db database.db = db
} else {
err := database.db.Ping()
handleErr(err)
} }
return database.db return database.db
} }
func (database *Database) invalidateAccessCache(worker int64) {
delete(database.submitAccessCache, worker)
delete(database.assignAccessCache, worker)
}

View File

@ -55,11 +55,17 @@ func (database *Database) SaveProject(project *Project, webhookSecret string) (i
"project": project, "project": project,
}).Trace("Database.saveProject INSERT project") }).Trace("Database.saveProject INSERT project")
database.projectCache[id] = project
return id, nil return id, nil
} }
func (database *Database) GetProject(id int64) *Project { func (database *Database) GetProject(id int64) *Project {
if database.projectCache[id] != nil {
return database.projectCache[id]
}
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, assign_rate, submit_rate motd, public, hidden, COALESCE(chain, 0), paused, assign_rate, submit_rate
@ -76,7 +82,9 @@ func (database *Database) GetProject(id int64) *Project {
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"id": id, "id": id,
"project": project, "project": project,
}).Trace("Database.saveProject SELECT project") }).Trace("Database.getProject SELECT project")
database.projectCache[id] = project
return project return project
} }
@ -132,6 +140,8 @@ func (database *Database) UpdateProject(project *Project) error {
"rowsAffected": rowsAffected, "rowsAffected": rowsAffected,
}).Trace("Database.updateProject UPDATE project") }).Trace("Database.updateProject UPDATE project")
database.projectCache[project.Id] = project
return nil return nil
} }

View File

@ -1,23 +1,24 @@
package storage package storage
import ( import (
"database/sql"
"errors" "errors"
"fmt" "github.com/lib/pq"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type Task struct { type Task struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Priority int64 `json:"priority"` Priority int16 `json:"priority"`
Project *Project `json:"project"` Project *Project `json:"project"`
Assignee int64 `json:"assignee"` Assignee int64 `json:"assignee"`
Retries int64 `json:"retries"` Retries int16 `json:"retries"`
MaxRetries int64 `json:"max_retries"` MaxRetries int16 `json:"max_retries"`
Status TaskStatus `json:"status"` Status TaskStatus `json:"status"`
Recipe string `json:"recipe"` Recipe string `json:"recipe"`
MaxAssignTime int64 `json:"max_assign_time"` MaxAssignTime int64 `json:"max_assign_time"`
AssignTime int64 `json:"assign_time"` AssignTime int64 `json:"assign_time"`
VerificationCount int64 `json:"verification_count"` VerificationCount int16 `json:"verification_count"`
} }
type TaskStatus int type TaskStatus int
@ -42,16 +43,62 @@ type SaveTaskRequest struct {
WorkerId int64 WorkerId int64
} }
func (database *Database) SaveTask(task *Task, project int64, hash64 int64, wid int64) error { func (database *Database) checkAccess(workerId, projectId int64, assign, submit bool) bool {
if database.submitAccessCache[workerId] == nil {
database.submitAccessCache[workerId] = make(map[int64]bool)
database.assignAccessCache[workerId] = make(map[int64]bool)
} else {
_, ok := database.submitAccessCache[workerId][projectId]
if ok {
if assign && !database.assignAccessCache[workerId][projectId] {
return false
}
if submit && !database.submitAccessCache[workerId][projectId] {
return false
}
return true
}
}
db := database.getDB() db := database.getDB()
res, err := db.Exec(fmt.Sprintf(` row := db.QueryRow(`SELECT role_assign, role_submit FROM worker_access
INSERT INTO task (project, max_retries, recipe, priority, max_assign_time, hash64,verification_count) WHERE worker=$1 and project=$2 AND NOT request`,
SELECT $1,$2,$3,$4,$5,NULLIF(%d, 0),$6 FROM worker_access workerId, projectId)
WHERE role_submit AND NOT request AND worker=$7 AND project=$1`, hash64),
project, task.MaxRetries, task.Recipe, task.Priority, task.MaxAssignTime, task.VerificationCount, var hasAssign, hasSubmit bool
wid) err := row.Scan(&hasAssign, &hasSubmit)
database.submitAccessCache[workerId][projectId] = hasSubmit
database.assignAccessCache[workerId][projectId] = hasAssign
if err != nil {
return false
}
if !hasAssign && assign {
return false
}
if !hasSubmit && submit {
return false
}
return true
}
func (database *Database) SaveTask(task *Task, project int64, hash64 int64, wid int64) error {
if !database.checkAccess(wid, project, false, true) {
return errors.New("unauthorized task submit")
}
db := database.getDB()
_, err := db.Exec(`INSERT INTO task
(project, max_retries, recipe, priority, max_assign_time, hash64, verification_count)
VALUES ($1,$2,$3,$4,$5,$6,$7)`,
project, task.MaxRetries, task.Recipe, task.Priority, task.MaxAssignTime,
makeNullableInt(hash64), task.VerificationCount)
if err != nil { if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{ logrus.WithError(err).WithFields(logrus.Fields{
"task": task, "task": task,
@ -59,45 +106,55 @@ func (database *Database) SaveTask(task *Task, project int64, hash64 int64, wid
return err return err
} }
rowsAffected, err := res.RowsAffected()
handleErr(err)
logrus.WithFields(logrus.Fields{
"rowsAffected": rowsAffected,
"task": task,
}).Trace("Database.saveTask INSERT task")
if rowsAffected == 0 {
return errors.New("unauthorized task submit")
}
return nil return nil
} }
func makeNullableInt(i int64) sql.NullInt64 {
if i == 0 {
return sql.NullInt64{Valid: false}
} else {
return sql.NullInt64{
Valid: true,
Int64: i,
}
}
}
func (database Database) BulkSaveTask(bulkSaveTaskReqs []SaveTaskRequest) []error { func (database Database) BulkSaveTask(bulkSaveTaskReqs []SaveTaskRequest) []error {
if !database.checkAccess(bulkSaveTaskReqs[0].WorkerId, bulkSaveTaskReqs[0].Project,
false, true) {
return []error{errors.New("unauthorized task submit")}
}
db := database.getDB() db := database.getDB()
txn, err := db.Begin()
handleErr(err)
errs := make([]error, len(bulkSaveTaskReqs)) errs := make([]error, len(bulkSaveTaskReqs))
for i, req := range bulkSaveTaskReqs { stmt, _ := txn.Prepare(pq.CopyIn(
res, err := db.Exec(fmt.Sprintf(` "task",
INSERT INTO task (project, max_retries, recipe, priority, max_assign_time, hash64,verification_count) "project", "max_retries", "recipe", "priority",
SELECT $1,$2,$3,$4,$5,NULLIF(%d, 0),$6 FROM worker_access "max_assign_time", "hash64", "verification_count",
WHERE role_submit AND NOT request AND worker=$7 AND project=$1`, req.Hash64), ))
req.Project, req.Task.MaxRetries, req.Task.Recipe, req.Task.Priority,
req.Task.MaxAssignTime, req.Task.VerificationCount,
req.WorkerId)
errs[i] = err
if res != nil { for i, req := range bulkSaveTaskReqs {
rowsAffected, _ := res.RowsAffected() _, err = stmt.Exec(req.Project, req.Task.MaxRetries, req.Task.Recipe,
if rowsAffected == 0 { req.Task.Priority, req.Task.MaxAssignTime, makeNullableInt(req.Hash64),
errs[i] = errors.New("unauthorized task submit") req.Task.VerificationCount)
} if err != nil {
errs[i] = err
} }
} }
_, err = stmt.Exec()
err = stmt.Close()
handleErr(err)
err = txn.Commit()
handleErr(err)
return errs return errs
} }
@ -107,7 +164,7 @@ func (database Database) ReleaseTask(id int64, workerId int64, result TaskResult
var taskUpdated bool var taskUpdated bool
if result == TR_OK { if result == TR_OK {
row := db.QueryRow(fmt.Sprintf(`SELECT release_task_ok(%d,%d,%d)`, workerId, id, verification)) row := db.QueryRow(`SELECT release_task_ok($1,$2,$3)`, workerId, id, verification)
err := row.Scan(&taskUpdated) err := row.Scan(&taskUpdated)
if err != nil { if err != nil {
@ -128,13 +185,6 @@ func (database Database) ReleaseTask(id int64, workerId int64, result TaskResult
taskUpdated = rowsAffected == 1 taskUpdated = rowsAffected == 1
} }
logrus.WithFields(logrus.Fields{
"taskUpdated": taskUpdated,
"taskId": id,
"workerId": workerId,
"verification": verification,
}).Trace("Database.ReleaseTask")
return taskUpdated return taskUpdated
} }
@ -161,35 +211,21 @@ func (database *Database) GetTaskFromProject(worker *Worker, projectId int64) *T
ORDER BY task.priority DESC ORDER BY task.priority DESC
LIMIT 1 LIMIT 1
) )
RETURNING task.id`, worker.Id, projectId) RETURNING task.id, task.priority, assignee, retries, max_retries,
status, recipe, max_assign_time, assign_time, verification_count`, worker.Id, projectId)
var id int64
err := row.Scan(&id)
database.assignMutex.Unlock() database.assignMutex.Unlock()
task := &Task{}
err := row.Scan(&task.Id, &task.Priority, &task.Assignee,
&task.Retries, &task.MaxRetries, &task.Status, &task.Recipe, &task.MaxAssignTime,
&task.AssignTime, &task.VerificationCount)
if err != nil { if err != nil {
return nil return nil
} }
row = db.QueryRow(` task.Project = database.GetProject(projectId)
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),
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
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

@ -71,31 +71,6 @@ func (database *Database) GetWorker(id int64) *Worker {
return worker return worker
} }
func (database *Database) GrantAccess(workerId int64, projectId int64) bool {
db := database.getDB()
res, err := db.Exec(`UPDATE worker_access SET
request=FALSE WHERE worker=$1 AND project=$2`,
workerId, projectId)
if err != nil {
logrus.WithFields(logrus.Fields{
"workerId": workerId,
"projectId": projectId,
}).WithError(err).Warn("Database.GrantAccess INSERT")
return false
}
rowsAffected, _ := res.RowsAffected()
logrus.WithFields(logrus.Fields{
"rowsAffected": rowsAffected,
"workerId": workerId,
"projectId": projectId,
}).Trace("Database.GrantAccess INSERT")
return rowsAffected == 1
}
func (database *Database) UpdateWorker(worker *Worker) bool { func (database *Database) UpdateWorker(worker *Worker) bool {
db := database.getDB() db := database.getDB()
@ -139,6 +114,7 @@ func (database *Database) SaveAccessRequest(wa *WorkerAccess) bool {
func (database *Database) AcceptAccessRequest(worker int64, projectId int64) bool { func (database *Database) AcceptAccessRequest(worker int64, projectId int64) bool {
db := database.getDB() db := database.getDB()
database.invalidateAccessCache(worker)
res, err := db.Exec(`UPDATE worker_access SET request=FALSE res, err := db.Exec(`UPDATE worker_access SET request=FALSE
WHERE worker=$1 AND project=$2`, worker, projectId) WHERE worker=$1 AND project=$2`, worker, projectId)
@ -156,6 +132,8 @@ func (database *Database) AcceptAccessRequest(worker int64, projectId int64) boo
func (database *Database) RejectAccessRequest(workerId int64, projectId int64) bool { func (database *Database) RejectAccessRequest(workerId int64, projectId int64) bool {
db := database.getDB() db := database.getDB()
database.invalidateAccessCache(workerId)
res, err := db.Exec(`DELETE FROM worker_access WHERE worker=$1 AND project=$2`, res, err := db.Exec(`DELETE FROM worker_access WHERE worker=$1 AND project=$2`,
workerId, projectId) workerId, projectId)
handleErr(err) handleErr(err)

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/simon987/task_tracker/api" "github.com/simon987/task_tracker/api"
"github.com/simon987/task_tracker/storage" "github.com/simon987/task_tracker/storage"
"golang.org/x/time/rate"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"testing" "testing"
@ -136,7 +137,7 @@ func TestUpdateProjectValid(t *testing.T) {
Hidden: true, Hidden: true,
Paused: true, Paused: true,
AssignRate: 1, AssignRate: 1,
SubmitRate: 2, SubmitRate: 0,
Version: "VersionB", Version: "VersionB",
}, pid, testAdminCtx) }, pid, testAdminCtx)
@ -173,7 +174,7 @@ func TestUpdateProjectValid(t *testing.T) {
if proj.Project.AssignRate != 1 { if proj.Project.AssignRate != 1 {
t.Error() t.Error()
} }
if proj.Project.SubmitRate != 2 { if proj.Project.SubmitRate != rate.Inf {
t.Error() t.Error()
} }
} }

View File

@ -2,6 +2,7 @@ package test
import ( import (
"github.com/simon987/task_tracker/api" "github.com/simon987/task_tracker/api"
"github.com/simon987/task_tracker/storage"
"strconv" "strconv"
"testing" "testing"
) )
@ -17,6 +18,13 @@ func BenchmarkCreateTaskRemote(b *testing.B) {
worker := genWid() worker := genWid()
requestAccess(api.CreateWorkerAccessRequest{
Submit: true,
Assign: false,
Project: resp.Content.Id,
}, worker)
acceptAccessRequest(resp.Content.Id, worker.Id, testAdminCtx)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
createTask(api.SubmitTaskRequest{ createTask(api.SubmitTaskRequest{
@ -27,3 +35,36 @@ func BenchmarkCreateTaskRemote(b *testing.B) {
}, worker) }, worker)
} }
} }
func BenchmarkCreateTask(b *testing.B) {
resp := createProjectAsAdmin(api.CreateProjectRequest{
Name: "BenchmarkCreateTask" + strconv.Itoa(b.N),
GitRepo: "benchmark_test" + strconv.Itoa(b.N),
Version: "f09e8c9r0w839x0c43",
CloneUrl: "http://localhost",
})
worker := genWid()
requestAccess(api.CreateWorkerAccessRequest{
Submit: true,
Assign: false,
Project: resp.Content.Id,
}, worker)
acceptAccessRequest(resp.Content.Id, worker.Id, testAdminCtx)
db := storage.New()
b.ResetTimer()
p := db.GetProject(resp.Content.Id)
for i := 0; i < b.N; i++ {
db.SaveTask(&storage.Task{
Project: p,
Priority: 0,
Recipe: "{}",
MaxRetries: 1,
}, resp.Content.Id, 0, worker.Id)
}
}

View File

@ -405,6 +405,7 @@ func TestReleaseTaskSuccess(t *testing.T) {
Project: pid, Project: pid,
Recipe: "{}", Recipe: "{}",
MaxRetries: 3, MaxRetries: 3,
Hash64: math.MaxInt64,
}, worker) }, worker)
task := getTaskFromProject(pid, worker).Content.Task task := getTaskFromProject(pid, worker).Content.Task
@ -923,25 +924,20 @@ func TestTaskSubmitInvalidDoesntGiveRateLimit(t *testing.T) {
func TestBulkTaskSubmitValid(t *testing.T) { func TestBulkTaskSubmitValid(t *testing.T) {
proj := createProjectAsAdmin(api.CreateProjectRequest{
Name: "testbulksubmit",
CloneUrl: "testbulkprojectsubmit",
GitRepo: "testbulkprojectsubmit",
}).Content.Id
r := bulkSubmitTask(api.BulkSubmitTaskRequest{ r := bulkSubmitTask(api.BulkSubmitTaskRequest{
Requests: []api.SubmitTaskRequest{ Requests: []api.SubmitTaskRequest{
{ {
Recipe: "1234", Recipe: "1234",
Project: proj, Project: testProject,
}, },
{ {
Recipe: "1234", Recipe: "1234",
Project: proj, Project: testProject,
}, },
{ {
Recipe: "1234", Recipe: "1234",
Project: proj, Project: testProject,
Hash64: 8565956259293726066,
}, },
}, },
}, testWorker) }, testWorker)
@ -1015,6 +1011,44 @@ func TestBulkTaskSubmitInvalid2(t *testing.T) {
} }
} }
func TestTaskGetUnauthorizedWithCache(t *testing.T) {
pid := createProjectAsAdmin(api.CreateProjectRequest{
Name: "testtaskgetunauthorizedcache",
GitRepo: "testtaskgetunauthorizedcache",
CloneUrl: "testtaskgettunauthorizedcache",
}).Content.Id
w := genWid()
requestAccess(api.CreateWorkerAccessRequest{
Project: pid,
Submit: true,
Assign: true,
}, w)
acceptAccessRequest(pid, w.Id, testAdminCtx)
r1 := createTask(api.SubmitTaskRequest{
Project: pid,
Recipe: "ssss",
}, w)
// removed access, cache should be invalidated
rejectAccessRequest(pid, w.Id, testAdminCtx)
r2 := createTask(api.SubmitTaskRequest{
Project: pid,
Recipe: "ssss",
}, w)
if r1.Ok != true {
t.Error()
}
if r2.Ok != false {
t.Error()
}
}
func bulkSubmitTask(request api.BulkSubmitTaskRequest, worker *storage.Worker) (ar api.JsonResponse) { func bulkSubmitTask(request api.BulkSubmitTaskRequest, worker *storage.Worker) (ar api.JsonResponse) {
r := Post("/task/bulk_submit", request, worker, nil) r := Post("/task/bulk_submit", request, worker, nil)
UnmarshalResponse(r, &ar) UnmarshalResponse(r, &ar)

25
test/schema.sql Executable file → Normal file
View File

@ -41,6 +41,8 @@ CREATE TABLE worker_access
request boolean, request boolean,
primary key (worker, project) primary key (worker, project)
); );
CREATE INDEX worker_index ON worker_access (worker);
CREATE INDEX project_index ON worker_access (project);
CREATE TABLE task CREATE TABLE task
( (
@ -50,22 +52,28 @@ CREATE TABLE task
assignee INTEGER REFERENCES worker (id), assignee INTEGER REFERENCES worker (id),
max_assign_time INTEGER DEFAULT 0, max_assign_time INTEGER DEFAULT 0,
assign_time INTEGER DEFAULT NULL, assign_time INTEGER DEFAULT NULL,
verification_count INTEGER DEFAULT 0, verification_count SMALLINT DEFAULT 0,
priority SMALLINT DEFAULT 0, priority SMALLINT DEFAULT 0,
retries SMALLINT DEFAULT 0, retries SMALLINT DEFAULT 0,
max_retries SMALLINT, max_retries SMALLINT,
status SMALLINT DEFAULT 1, status SMALLINT DEFAULT 1,
recipe TEXT, recipe TEXT
UNIQUE (project, hash64)
); );
CREATE INDEX priority_desc_index ON task (priority DESC);
CREATE INDEX assignee_index ON task (assignee);
CREATE INDEX verifcnt_index ON task (verification_count);
CREATE UNIQUE INDEX project_hash_unique ON task (project, hash64);
CREATE TABLE worker_verifies_task CREATE TABLE worker_verifies_task
( (
verification_hash BIGINT NOT NULL, verification_hash BIGINT NOT NULL,
task BIGINT REFERENCES task (id) ON DELETE CASCADE NOT NULL, task INT REFERENCES task (id) ON DELETE CASCADE NOT NULL,
worker INT REFERENCES worker (id) NOT NULL worker INT REFERENCES worker (id) NOT NULL
); );
CREATE INDEX task_index ON worker_verifies_task (task);
CREATE TABLE log_entry CREATE TABLE log_entry
( (
level INTEGER NOT NULL, level INTEGER NOT NULL,
@ -150,7 +158,7 @@ $$
DECLARE DECLARE
res INT = NULL; res INT = NULL;
BEGIN BEGIN
DELETE FROM task WHERE id = tid AND assignee = wid AND verification_count < 2 RETURNING project INTO res; DELETE FROM task WHERE id = tid AND assignee = wid AND verification_count = 1 RETURNING project INTO res;
IF res IS NULL THEN IF res IS NULL THEN
INSERT INTO worker_verifies_task (worker, verification_hash, task) INSERT INTO worker_verifies_task (worker, verification_hash, task)
@ -171,11 +179,10 @@ BEGIN
LIMIT 1) >= task.verification_count RETURNING task.id INTO res; LIMIT 1) >= task.verification_count RETURNING task.id INTO res;
IF res IS NULL THEN IF res IS NULL THEN
UPDATE task SET assignee= NULL WHERE id = tid AND assignee = wid; UPDATE task SET assignee=NULL WHERE id = tid AND assignee = wid;
end if; end if;
end if; end if;
RETURN res IS NOT NULL; RETURN res IS NOT NULL;
END; END;
$$ LANGUAGE 'plpgsql'; $$ LANGUAGE 'plpgsql';

0
web/angular/e2e/protractor.conf.js Executable file → Normal file
View File

0
web/angular/e2e/src/app.e2e-spec.ts Executable file → Normal file
View File

0
web/angular/e2e/src/app.po.ts Executable file → Normal file
View File

0
web/angular/e2e/tsconfig.e2e.json Executable file → Normal file
View File

View File

@ -4323,9 +4323,9 @@
} }
}, },
"fstream": { "fstream": {
"version": "1.0.11", "version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
"integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"requires": { "requires": {
@ -6050,9 +6050,9 @@
} }
}, },
"lodash": { "lodash": {
"version": "4.17.11", "version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
}, },
"lodash.clonedeep": { "lodash.clonedeep": {
"version": "4.5.0", "version": "4.5.0",
@ -6519,9 +6519,9 @@
} }
}, },
"mixin-deep": { "mixin-deep": {
"version": "1.3.1", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
"integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
"dev": true, "dev": true,
"requires": { "requires": {
"for-in": "^1.0.2", "for-in": "^1.0.2",
@ -8514,9 +8514,9 @@
"dev": true "dev": true
}, },
"set-value": { "set-value": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
"integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
"dev": true, "dev": true,
"requires": { "requires": {
"extend-shallow": "^2.0.1", "extend-shallow": "^2.0.1",
@ -9366,14 +9366,14 @@
"dev": true "dev": true
}, },
"tar": { "tar": {
"version": "2.2.1", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
"integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"requires": { "requires": {
"block-stream": "*", "block-stream": "*",
"fstream": "^1.0.2", "fstream": "^1.0.12",
"inherits": "2" "inherits": "2"
} }
}, },
@ -9881,38 +9881,15 @@
"dev": true "dev": true
}, },
"union-value": { "union-value": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
"integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
"dev": true, "dev": true,
"requires": { "requires": {
"arr-union": "^3.1.0", "arr-union": "^3.1.0",
"get-value": "^2.0.6", "get-value": "^2.0.6",
"is-extendable": "^0.1.1", "is-extendable": "^0.1.1",
"set-value": "^0.4.3" "set-value": "^2.0.1"
},
"dependencies": {
"extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"dev": true,
"requires": {
"is-extendable": "^0.1.0"
}
},
"set-value": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
"integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
"dev": true,
"requires": {
"extend-shallow": "^2.0.1",
"is-extendable": "^0.1.1",
"is-plain-object": "^2.0.1",
"to-object-path": "^0.3.0"
}
}
} }
}, },
"unique-filename": { "unique-filename": {

View File

@ -25,7 +25,7 @@
"@ngx-translate/http-loader": "^4.0.0", "@ngx-translate/http-loader": "^4.0.0",
"chart.js": "^2.7.3", "chart.js": "^2.7.3",
"core-js": "^2.5.4", "core-js": "^2.5.4",
"lodash": "^4.17.11", "lodash": "^4.17.15",
"moment": "^2.23.0", "moment": "^2.23.0",
"rxjs": "~6.3.3", "rxjs": "~6.3.3",
"tslib": "^1.9.0", "tslib": "^1.9.0",

0
web/angular/src/app/api.service.ts Executable file → Normal file
View File

0
web/angular/src/app/app-routing.module.ts Executable file → Normal file
View File

0
web/angular/src/app/app.component.css Executable file → Normal file
View File

0
web/angular/src/app/app.component.html Executable file → Normal file
View File

0
web/angular/src/app/app.module.ts Executable file → Normal file
View File

View File

View File

View File

View File

@ -115,7 +115,7 @@
"subtitle": "Real-time data for all projects", "subtitle": "Real-time data for all projects",
"paused": "(paused)", "paused": "(paused)",
"pause": "Pause", "pause": "Pause",
"manage": "Manager workers" "manage": "Manage workers"
}, },
"perms": { "perms": {
"title": "Project permissions", "title": "Project permissions",