update design

This commit is contained in:
criyle 2019-09-29 15:19:36 -07:00
parent 10ba885724
commit 9f1f7de770
15 changed files with 127 additions and 78 deletions

View File

@ -6,22 +6,30 @@ The goal to to reimplement [syzoj/judge-v3](https://github.com/syzoj/judge-v3) i
## Planned Design ## Planned Design
Container Root Filesystem:
+ necessary lib / exec / compiler / header readonly bind mounted from current file system: /lib /lib64 /bin /usr
+ work directory tmpfs mount: /w (work dir), /tmp (compiler temp files)
+ additional compiler scripts / exec readonly bind mounted: /c
+ additional header readonly bind mounted: /i
Brokers and interfaces: Brokers and interfaces:
+ client: receive pushed judge tasks from website (web-socket / socket.io / RabbitMQ) + client: receive pushed judge tasks from website (websocket / socket.io / RabbitMQ)
+ data: interface to download, cache and access test files from website by id + data: interface to download, cache, lock and access test data files from website by data_id
+ taskqueue: send to and receive from a queue to run task (GO channel / (RabbitMQ, Redis)) and also including upload / download executable files from compile task + taskqueue: message queue to send run task and receive result (GO channel / (RabbitMQ, Redis))
+ file: general file interface (local / memory) + file: general file interface (disk / memory)
+ language: language configuration for runner + language: programming language compile & execute configurations
Workers: Workers:
+ judger: execute judge tasks and distribute as run task to queue + judger: execute judge logics (compile / standard / interactive / answer submit) and distribute as run task to queue
+ runner: receive run task and execute in sandbox (compile / standard / interactive / answer submit) + runner: receive run task and execute in sandbox (dumb runner)
Models: Models:
+ JudgeTask: judge task pushed from website (type, data) + JudgeTask: judge task pushed from website (type, source, data)
+ JudgeResult: judge task result send back to website
+ JudgeSetting: problem setting (from yaml) and JudgeCase + JudgeSetting: problem setting (from yaml) and JudgeCase
+ RunTask: run task parameters send to run_queue + RunTask: run task parameters send to run_queue
+ RunResult: run task result sent back from queue + RunResult: run task result sent back from queue

View File

@ -4,14 +4,19 @@ import "github.com/criyle/go-judge/types"
// Task contains a single task received from web // Task contains a single task received from web
type Task interface { type Task interface {
// Param get the judge task
Param() *types.JudgeTask Param() *types.JudgeTask
Progress(p types.JudgeProgress)
Finish(p types.JudgeResult) // Progress emits current progress to website
Progress(*types.JudgeProgress)
// Finish emits the final result to website
Finish(*types.JudgeResult)
} }
// Client should connect to web service and receive works from web // Client should connect to web service and receive works from web
// it should sent received work through go channel (have background goroutine(s)) // it should sent received work through go channel (have background goroutine(s))
type Client interface { type Client interface {
// C should return channel to receive works // C return channel to receive works
C() <-chan Task C() <-chan Task
} }

View File

@ -5,7 +5,13 @@ import "os"
// File defines file name with its content // File defines file name with its content
// file could on file system or memory // file could on file system or memory
type File interface { type File interface {
// Name get the file name
Name() string Name() string
Content() ([]byte, error) // get content of the file
Open() (*os.File, error) // get readonly fd of the file // Content reads the file content
Content() ([]byte, error)
// Open opens the file in readonly mode
// caller should close afterwards
Open() (*os.File, error)
} }

View File

@ -3,23 +3,26 @@ package localfile
import ( import (
"io/ioutil" "io/ioutil"
"os" "os"
"github.com/criyle/go-judge/file"
) )
// File stores a path to represent a local file // File stores a path to represent a local file
type File struct { type File struct {
path string name, path string
} }
// New creats a wrapper to path // New creates a wrapper to file system by path
func New(path string) *File { func New(name, path string) file.File {
return &File{ return &File{
name: name,
path: path, path: path,
} }
} }
// Name get the path // Name get the path
func (f *File) Name() string { func (f *File) Name() string {
return f.path return f.name
} }
// Content reads file content // Content reads file content

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"os" "os"
"github.com/criyle/go-judge/file"
"github.com/criyle/go-sandbox/pkg/memfd" "github.com/criyle/go-sandbox/pkg/memfd"
) )
@ -13,9 +14,8 @@ type File struct {
content []byte content []byte
} }
// New create a file interface, content should not be modified after // New create a file interface from content and content should not be modified afterwards
// NewMemFile func New(name string, content []byte) file.File {
func New(name string, content []byte) *File {
return &File{ return &File{
name: name, name: name,
content: content, content: content,

2
go.mod
View File

@ -2,4 +2,4 @@ module github.com/criyle/go-judge
go 1.12 go 1.12
require github.com/criyle/go-sandbox v0.0.0-20190915221016-9898c301d566 require github.com/criyle/go-sandbox v0.0.0-20190929004305-4001c7c7671f

8
go.sum
View File

@ -1,5 +1,5 @@
github.com/criyle/go-sandbox v0.0.0-20190915221016-9898c301d566 h1:JB33qewgHSIHojNHn9tnFu1HaBwRkh8zBDP0LAu6Ars= github.com/criyle/go-sandbox v0.0.0-20190929004305-4001c7c7671f h1:2VJ5GhYvEEXOwIeRF2SNepXp5TQ98ZjELGIriGWQIps=
github.com/criyle/go-sandbox v0.0.0-20190915221016-9898c301d566/go.mod h1:V5llzm3jeoB8pq2XEwX/tSVK3RQxjPl+vTSBDFGUO4A= github.com/criyle/go-sandbox v0.0.0-20190929004305-4001c7c7671f/go.mod h1:xAafqqJsAlGuL8gMgnrbNtYLmCtAWzOZQ4kM9y6/zdk=
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
golang.org/x/sys v0.0.0-20190913121621-c3b328c6e5a7 h1:wYqz/tQaWUgGKyx+B/rssSE6wkIKdY5Ee6ryOmzarIg= golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA=
golang.org/x/sys v0.0.0-20190913121621-c3b328c6e5a7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -9,6 +9,6 @@ import (
// Judger receives task from client and translate to task for runner // Judger receives task from client and translate to task for runner
type Judger struct { type Judger struct {
client.Client client.Client
taskqueue.Queue taskqueue.Sender
problem.Builder problem.Builder
} }

View File

@ -30,11 +30,11 @@ loop:
} }
} }
func (j *Judger) run(done <-chan struct{}, t client.Task) types.JudgeResult { func (j *Judger) run(done <-chan struct{}, t client.Task) *types.JudgeResult {
var result types.JudgeResult var result types.JudgeResult
errResult := func(err error) types.JudgeResult { errResult := func(err error) *types.JudgeResult {
result.Error = err.Error() result.Error = err.Error()
return result return &result
} }
p := t.Param() p := t.Param()
@ -45,7 +45,7 @@ func (j *Judger) run(done <-chan struct{}, t client.Task) types.JudgeResult {
// compile // compile
compileRet := make(chan types.RunTaskResult) compileRet := make(chan types.RunTaskResult)
err = j.Enqueue(types.RunTask{ err = j.Send(types.RunTask{
Type: "compile", Type: "compile",
Language: p.Language, Language: p.Language,
Code: p.Code, Code: p.Code,
@ -78,7 +78,7 @@ func (j *Judger) run(done <-chan struct{}, t client.Task) types.JudgeResult {
for range pconf.Subtasks { for range pconf.Subtasks {
result.SubTasks = append(result.SubTasks, <-subTaskResult) result.SubTasks = append(result.SubTasks, <-subTaskResult)
} }
return result return &result
} }
type problemJudger struct { type problemJudger struct {
@ -94,12 +94,12 @@ func (pj *problemJudger) runSubtask(done <-chan struct{}, exec []file.File, s *t
var result types.JudgeSubTaskResult var result types.JudgeSubTaskResult
caseResult := make(chan types.RunTaskResult, len(s.Cases)) caseResult := make(chan types.RunTaskResult, len(s.Cases))
for _, c := range s.Cases { for _, c := range s.Cases {
pj.Enqueue(types.RunTask{ pj.Send(types.RunTask{
Type: pj.ProblemConfig.Type, Type: pj.ProblemConfig.Type,
Language: pj.Language, Language: pj.Language,
TimeLimit: pj.TileLimit, TimeLimit: pj.TileLimit,
MemoryLimit: pj.MemoryLimit, MemoryLimit: pj.MemoryLimit,
Executables: exec, ExtraFiles: exec,
InputFile: c.Input, InputFile: c.Input,
AnswerFile: c.Answer, AnswerFile: c.Answer,
}, caseResult) }, caseResult)
@ -121,7 +121,7 @@ func (pj *problemJudger) runSubtask(done <-chan struct{}, exec []file.File, s *t
result.Score += rt.ScoringRate result.Score += rt.ScoringRate
// report prograss // report prograss
atomic.AddInt32(&pj.count, 1) atomic.AddInt32(&pj.count, 1)
pj.Progress(types.JudgeProgress{ pj.Progress(&types.JudgeProgress{
Message: fmt.Sprintf("%d/%d", pj.count, pj.total), Message: fmt.Sprintf("%d/%d", pj.count, pj.total),
}) })
} }

View File

@ -7,17 +7,17 @@ import (
) )
type pool struct { type pool struct {
queue chan *daemon.Master builder *daemon.Builder
count int32 queue chan *daemon.Master
root string count int32
} }
const maxPoolSize = 64 const maxPoolSize = 64
func newPool(root string) *pool { func newPool(builder *daemon.Builder) *pool {
return &pool{ return &pool{
queue: make(chan *daemon.Master, maxPoolSize), queue: make(chan *daemon.Master, maxPoolSize),
root: root, builder: builder,
} }
} }
@ -28,7 +28,7 @@ func (p *pool) Get() (*daemon.Master, error) {
default: default:
} }
atomic.AddInt32(&p.count, 1) atomic.AddInt32(&p.count, 1)
return daemon.New(p.root) return p.builder.Build()
} }
func (p *pool) Put(master *daemon.Master) { func (p *pool) Put(master *daemon.Master) {

View File

@ -21,7 +21,9 @@ const cgroupPrefix = "go-judge"
const minCPUPercent = 40 // 40% const minCPUPercent = 40 // 40%
const checkIntervalMS = 50 const checkIntervalMS = 50
var env = []string{"PATH=/usr/local/bin:/usr/bin:/bin"} var env = []string{
"PATH=/usr/local/bin:/usr/bin:/bin",
}
func (r *Runner) run(done <-chan struct{}, task *types.RunTask) *types.RunTaskResult { func (r *Runner) run(done <-chan struct{}, task *types.RunTask) *types.RunTaskResult {
t := language.TypeExec t := language.TypeExec
@ -80,7 +82,7 @@ func (r *Runner) run(done <-chan struct{}, task *types.RunTask) *types.RunTaskRe
SyncFunc: cg.AddProc, SyncFunc: cg.AddProc,
} }
// copyin source code for compile or executables for exec // copyin source code for compile or exec files for exec
if t == language.TypeCompile { if t == language.TypeCompile {
source := memfile.New("source", []byte(task.Code)) source := memfile.New("source", []byte(task.Code))
sourceFile, err := source.Open() sourceFile, err := source.Open()
@ -92,7 +94,7 @@ func (r *Runner) run(done <-chan struct{}, task *types.RunTask) *types.RunTaskRe
return errResult(err.Error()) return errResult(err.Error())
} }
} else { } else {
for _, f := range task.Executables { for _, f := range task.ExecFiles {
execFile, err := f.Open() execFile, err := f.Open()
if err != nil { if err != nil {
return errResult(err.Error()) return errResult(err.Error())
@ -131,7 +133,7 @@ func (r *Runner) run(done <-chan struct{}, task *types.RunTask) *types.RunTaskRe
var lastCPUUsage uint64 var lastCPUUsage uint64
var totalTime time.Duration var totalTime time.Duration
var rt stypes.Result var rt stypes.Result
var rtreceived bool var rtReceived bool
lastCheckTime := time.Now() lastCheckTime := time.Now()
// wait task finish // wait task finish
@ -156,7 +158,7 @@ loop:
lastCPUUsage = cpuUsage lastCPUUsage = cpuUsage
case rt = <-rc: // returned case rt = <-rc: // returned
rtreceived = true rtReceived = true
break loop break loop
case <-outputPipe.Done: // outputlimit exceeded case <-outputPipe.Done: // outputlimit exceeded
@ -169,7 +171,7 @@ loop:
// get result if did not received // get result if did not received
cancelC.cancel() cancelC.cancel()
if !rtreceived { if !rtReceived {
rt = <-rc rt = <-rc
} }

View File

@ -5,38 +5,49 @@ import (
"github.com/criyle/go-judge/language" "github.com/criyle/go-judge/language"
"github.com/criyle/go-judge/taskqueue" "github.com/criyle/go-judge/taskqueue"
"github.com/criyle/go-sandbox/daemon"
) )
// Runner is the task runner // Runner is the task runner
type Runner struct { type Runner struct {
Queue taskqueue.Queue // Queue is the message queue to receive run tasks
Queue taskqueue.Receiver
// Builder builds the sandbox runner
Builder *daemon.Builder
Language language.Language Language language.Language
Root string
pool *pool // pool of sandbox to use
once sync.Once pool *pool
once sync.Once
} }
func (r *Runner) init() { func (r *Runner) init() {
r.pool = newPool(r.Root) r.pool = newPool(r.Builder)
} }
// Loop status a runner in a forever loop, waiting for task and execute // Loop status a runner in a forever loop, waiting for task and execute
// call it in new goroutine // call it in new goroutine
func (r *Runner) Loop(done <-chan struct{}) error { func (r *Runner) Loop(done <-chan struct{}) error {
r.once.Do(r.init) r.once.Do(r.init)
c := r.Queue.C() c := r.Queue.ReceiveC()
loop: loop:
for { for {
select { select {
case <-done: case <-done:
break loop break loop
case task := <-c: case task := <-c:
select { task.Done(r.run(done, task.Task()))
case <-done: }
break loop
default: // check if cancel is signaled
task.Finish(r.run(done, task.Task())) select {
} case <-done:
break loop
default:
} }
} }
return nil return nil

View File

@ -7,20 +7,20 @@ import (
const buffSize = 512 const buffSize = 512
// Queue implements taskqueue by go channel // Queue implements taskqueue by buffered go channel
type Queue struct { type Queue struct {
queue chan taskqueue.Task queue chan taskqueue.Task
} }
// New craetes new Queue with buffed go channel // New creates new Queue with buffed go channel
func New() *Queue { func New() taskqueue.Queue {
return &Queue{ return &Queue{
queue: make(chan taskqueue.Task, buffSize), queue: make(chan taskqueue.Task, buffSize),
} }
} }
// Enqueue puts task into run queue // Send puts task into run queue
func (q *Queue) Enqueue(t types.RunTask, r chan<- types.RunTaskResult) error { func (q *Queue) Send(t types.RunTask, r chan<- types.RunTaskResult) error {
q.queue <- Task{ q.queue <- Task{
task: t, task: t,
result: r, result: r,
@ -28,8 +28,8 @@ func (q *Queue) Enqueue(t types.RunTask, r chan<- types.RunTaskResult) error {
return nil return nil
} }
// C returns the underlying channel // ReceiveC returns the underlying channel
func (q *Queue) C() <-chan taskqueue.Task { func (q *Queue) ReceiveC() <-chan taskqueue.Task {
return q.queue return q.queue
} }
@ -44,7 +44,7 @@ func (t Task) Task() *types.RunTask {
return &t.task return &t.task
} }
// Finish returns the run task result // Done returns the run task result
func (t Task) Finish(r *types.RunTaskResult) { func (t Task) Done(r *types.RunTaskResult) {
t.result <- *r t.result <- *r
} }

View File

@ -2,15 +2,29 @@ package taskqueue
import "github.com/criyle/go-judge/types" import "github.com/criyle/go-judge/types"
// Queue provides asynchronous message queue to run execution tasks // Sender interface is used to send run task into message queue
type Sender interface {
// Send used to initial a RPC call and receive result to channel (should have enough space)
Send(types.RunTask, chan<- types.RunTaskResult) error
}
// Receiver interface is used to receive run task from message queue
type Receiver interface {
// ReceiveC get the channel to receive tasks
ReceiveC() <-chan Task
}
// Queue provides asynchronous message queue for run tasks
type Queue interface { type Queue interface {
// Enqueue used to initial a RPC call and receive result to channel Sender
Enqueue(types.RunTask, chan<- types.RunTaskResult) error Receiver
C() <-chan Task
} }
// Task represent a single task to run // Task represent a single task to run
type Task interface { type Task interface {
// Task gets the run task parameter
Task() *types.RunTask Task() *types.RunTask
Finish(*types.RunTaskResult)
// Done returns the run task result for the run task (should be called only once at end)
Done(*types.RunTaskResult)
} }

View File

@ -15,17 +15,17 @@ type RunTask struct {
// Used for run tasks // Used for run tasks
TimeLimit uint64 // ms TimeLimit uint64 // ms
MemoryLimit uint64 // kb MemoryLimit uint64 // kb
Executables []file.File ExecFiles []file.File
InputFile file.File InputFile file.File
AnswerFile file.File AnswerFile file.File
// Used for standard run task // Used for standard run task
SpjLanguage string SpjLanguage string
SpjExecutables []file.File SpjExecFiles []file.File
// Used for interaction run task // Used for interaction run task
InteractorLanguage string InteractorLanguage string
InteractorExecutables []file.File InteractorExecFiles []file.File
// Used for answer submission run task // Used for answer submission run task
UserAnswer file.File UserAnswer file.File