feat: #6 [7] Rewrite run_judge

This commit is contained in:
Paul Pan 2024-01-06 19:21:37 +08:00
parent 8dee13af85
commit bb105d1451
Signed by: Paul
GPG Key ID: D639BDF5BA578AF4
13 changed files with 244 additions and 314 deletions

View File

@ -56,14 +56,8 @@ func (h *handler) Judge(_ context.Context, t *asynq.Task) error {
return e.Success, 0, compileResult
}
// 4. config
config, err := h.runnerService.ParseConfig(p.ProblemVersionID, true)
if err != nil {
return e.InternalError, 0, systemError
}
// 5. run and judge
result, point, status := h.runnerService.RunAndJudge(p.ProblemVersionID, user, p.Submission.Language, &config)
// 4. run and judge
result, point, status := h.runnerService.RunAndJudge(p.ProblemVersionID, user, p.Submission.Language)
return utils.If(status != e.Success, e.InternalError, e.Success), point, result
}()

View File

@ -30,31 +30,6 @@ func init() {
TmpDir = path.Join(Prefix, TmpDir)
}
func (s *service) checkAndExecute(version uint, user string, lang string, script string, fail e.Status) e.Status {
if !s.ProblemExists(version) {
s.log.Info("problem not exists", zap.Uint("version", version))
return e.RunnerProblemNotExist
}
if !s.userExists(user, fmt.Sprintf("%s.%s", user, lang)) {
s.log.Info("user program not exists", zap.String("user", user), zap.String("lang", lang))
return e.RunnerUserNotExist
}
err := s.execute(script, fmt.Sprintf("%d", version), user, lang)
if err != nil {
s.log.Info("execute failed",
zap.Error(err),
zap.Uint("version", version),
zap.String("user", user),
zap.String("lang", lang))
return fail
}
return e.Success
}
func (s *service) check(version uint, user string, lang string) e.Status {
if !s.ProblemExists(version) {
s.log.Info("problem not exists", zap.Uint("version", version))
@ -78,3 +53,20 @@ func (s *service) userExists(user string, name string) bool {
userPath := filepath.Join(UserDir, user, name)
return file.Exist(userPath)
}
func (s *service) getLangInfo(config *Config, lang string) (configLanguage, bool) {
for _, l := range config.Languages {
if l.Lang == lang {
return l, true
}
}
return configLanguage{}, false
}
func (s *service) getLangScript(l *configLanguage, lang string) string {
if l.Type == "default" {
return "/woj/framework/template/default/" + lang + ".Makefile"
} else {
return "/woj/problem/judge/" + l.Script
}
}

View File

@ -12,30 +12,6 @@ import (
"time"
)
func (s *service) getProblemScript(version uint, lang string) (string, e.Status) {
config, err := s.ParseConfig(version, true)
if err != nil {
return "", e.RunnerProblemParseFailed
}
var script string
for _, l := range config.Languages {
if l.Lang == lang {
if l.Type == "default" {
script = "/woj/framework/template/default/" + lang + ".Makefile"
} else {
script = "/woj/problem/judge/" + l.Script
}
break
}
}
if script == "" {
return "", e.RunnerProblemParseFailed
}
return script, e.Success
}
func (s *service) Compile(version uint, user string, lang string) (JudgeStatus, e.Status) {
// 1. ensure problem/user exists
status := s.check(version, user, lang)
@ -43,15 +19,17 @@ func (s *service) Compile(version uint, user string, lang string) (JudgeStatus,
return JudgeStatus{Message: "check failed"}, status
}
config, err := s.ParseConfig(version, true)
if err != nil {
return JudgeStatus{Message: "parse config failed"}, e.RunnerProblemParseFailed
}
// 2. prepare judge environment
workDir := filepath.Join(UserDir, user)
judgeDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "judge")
sourceFile := filepath.Join(workDir, fmt.Sprintf("%s.%s", user, lang))
boxSourceFile := filepath.Join("/woj/problem/user", fmt.Sprintf("%s.%s", user, lang))
targetFile := filepath.Join(workDir, fmt.Sprintf("%s.out", user))
boxTargetFile := filepath.Join("/woj/problem/user", fmt.Sprintf("%s.out", user))
logFile := filepath.Join(workDir, fmt.Sprintf("%s.compile.log", user))
log, err := os.Create(logFile)
@ -64,25 +42,23 @@ func (s *service) Compile(version uint, user string, lang string) (JudgeStatus,
// 3. compile
err = utils.NewMust().
Do(func() error {
_ = os.Remove(targetFile)
return nil
}).
DoAny(func() error { return os.Remove(targetFile) }).
Do(func() error { return file.TouchErr(targetFile) }).
Do(func() error {
script, err := s.getProblemScript(version, lang)
if err != e.Success {
return err.AsError()
l, ok := s.getLangInfo(&config, lang)
script := s.getLangScript(&l, lang)
if !ok {
return e.RunnerProblemParseFailed.AsError()
}
args := []string{
"-v", judgeDir + ":/woj/problem/judge:ro",
"-v", sourceFile + ":" + boxSourceFile + ":ro",
"-v", targetFile + ":" + boxTargetFile,
"-v", fmt.Sprintf("%s:/woj/problem/judge:ro", judgeDir),
"-v", fmt.Sprintf("%s:/woj/user/%s.%s:ro", sourceFile, user, lang),
"-v", fmt.Sprintf("%s:/woj/user/%s.out", targetFile, user),
"-e", fmt.Sprintf("USER_PROG=%s", user),
"-e", fmt.Sprintf("LANG=%s", lang),
"git.0x7f.app/woj/ubuntu-full:latest",
"sh", "-c", fmt.Sprintf("cd /woj/problem/user && make -f %s compile", script),
"sh", "-c", fmt.Sprintf("cd /woj/user && make -f %s compile", script),
}
runArgs := &podmanArgs{

View File

@ -8,18 +8,20 @@ import (
"path/filepath"
)
type configLanguage struct {
Lang string `json:"Lang"`
Type string `json:"Type,omitempty"`
Script string `json:"Script,omitempty"`
Cmp string `json:"Cmp,omitempty"`
}
type Config struct {
Runtime struct {
TimeLimit int `json:"TimeLimit"`
MemoryLimit int `json:"MemoryLimit"`
NProcLimit int `json:"NProcLimit"`
} `json:"Runtime"`
Languages []struct {
Lang string `json:"Lang"`
Type string `json:"Type,omitempty"`
Script string `json:"Script,omitempty"`
Cmp string `json:"Cmp,omitempty"`
} `json:"Languages"`
Languages []configLanguage `json:"Languages"`
Tasks []struct {
Id int `json:"Id"`
Points int32 `json:"Points"`

View File

@ -53,8 +53,8 @@ func (s *service) prebuild(version uint, force bool) e.Status {
judgeDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "judge")
args := []string{
"-v", dataDir + ":/woj/problem/data",
"-v", judgeDir + ":/woj/problem/judge",
"-v", fmt.Sprintf("%s:/woj/problem/data", dataDir),
"-v", fmt.Sprintf("%s:/woj/problem/judge", judgeDir),
"-e", "PREFIX=/woj/problem",
"git.0x7f.app/woj/ubuntu-full:latest",
"sh", "-c", "cd /woj/problem/judge && make -f prebuild.Makefile prebuild && touch .mark.prebuild",

View File

@ -1,22 +0,0 @@
package runner
import "git.0x7f.app/WOJ/woj-server/internal/e"
func (s *service) RunAndJudge(version uint, user string, lang string, config *Config) (JudgeStatus, int32, e.Status) {
// run user program
status := s.checkAndExecute(version, user, lang, "problem_run.sh", e.RunnerRunFailed)
if status != e.Success {
return JudgeStatus{Message: "run failed"}, 0, status
}
// run judger
status = s.checkAndExecute(version, user, lang, "problem_judge.sh", e.RunnerJudgeFailed)
if status != e.Success {
return JudgeStatus{Message: "judge failed"}, 0, status
}
// check result
result, pts := s.checkResults(user, config)
return result, pts, e.Success
}

View File

@ -0,0 +1,183 @@
package runner
import (
"fmt"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/pkg/file"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"go.uber.org/zap"
"os"
"path/filepath"
"time"
)
func (s *service) problemRun(version uint, user string, lang string, config *Config) {
workDir := filepath.Join(UserDir, user)
dataDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "data", "input")
// woj-sandbox killer will add 2 more seconds, here we add 2 more seconds
timeout := time.Duration((config.Runtime.TimeLimit+1000)/1000+2+2) * time.Second
ids := make([]int, 0)
for _, task := range config.Tasks {
f := func(id int) func() {
return func() {
testCase := filepath.Join(dataDir, fmt.Sprintf("%d.input", id))
targetFile := filepath.Join(workDir, fmt.Sprintf("%s.out", user))
ansFile := filepath.Join(workDir, fmt.Sprintf("%d.out.usr", id))
ifoFile := filepath.Join(workDir, fmt.Sprintf("%d.info", id))
err := utils.NewMust().
DoAny(func() error { return os.Remove(ansFile) }).
DoAny(func() error { return os.Remove(ifoFile) }).
Do(func() error { return file.TouchErr(ansFile) }).
Do(func() error { return file.TouchErr(ifoFile) }).
Do(func() error {
args := []string{
"--cpus", "1",
"--network", "none",
"-v", fmt.Sprintf("%s:/woj/problem/data/input/%d.input:ro", testCase, id),
"-v", fmt.Sprintf("%s:/woj/user/%s.out:ro", targetFile, user),
"-v", fmt.Sprintf("%s:/woj/user/%d.out.usr", ansFile, id),
"-v", fmt.Sprintf("%s:/woj/user/%d.info", ifoFile, id),
"git.0x7f.app/woj/ubuntu-run:latest",
"sh", "-c",
fmt.Sprintf("cd /woj/user && /woj/framework/scripts/woj_launcher "+
"--memory_limit=%d "+
"--nproc_limit=%d "+
"--time_limit=%d "+
"--sandbox_template=%s "+
"--sandbox_action=ret "+
"--uid=1000 "+
"--gid=1000 "+
"--file_input=/woj/problem/data/input/%d.input "+
"--file_output=/woj/user/%d.out.usr "+
"--file_info=/woj/user/%d.info "+
"-program=/woj/user/%s.out",
config.Runtime.MemoryLimit,
config.Runtime.NProcLimit,
config.Runtime.TimeLimit,
lang,
id, id, id,
user,
),
}
runArgs := &podmanArgs{
executeArgs: executeArgs{
args: args,
timeout: timeout,
},
}
return s.podmanRun(runArgs)
}).
Done()
if err != nil {
s.log.Info("run failed",
zap.Error(err),
zap.Uint("version", version),
zap.String("user", user),
zap.String("lang", lang),
)
}
}
}(task.Id)
id := s.pool.AddTask(f)
ids = append(ids, id)
}
for _, id := range ids {
s.pool.WaitForTask(id)
}
}
func (s *service) problemJudge(version uint, user string, lang string, config *Config) {
workDir := filepath.Join(UserDir, user)
dataDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "data")
judgeDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "judge")
ids := make([]int, 0)
for _, task := range config.Tasks {
f := func(id int) func() {
return func() {
ansFile := filepath.Join(workDir, fmt.Sprintf("%d.out.usr", id))
jdgFile := filepath.Join(workDir, fmt.Sprintf("%d.judge", id))
c, ok := s.getLangInfo(config, lang)
if !ok {
return
}
err := utils.NewMust().
DoAny(func() error { return os.Remove(jdgFile) }).
Do(func() error { return file.TouchErr(jdgFile) }).
Do(func() error {
args := []string{
"-v", fmt.Sprintf("%s:/woj/problem/judge:ro", judgeDir),
"-v", fmt.Sprintf("%s:/woj/problem/data:ro", dataDir),
"-v", fmt.Sprintf("%s:/woj/user/%d.out.usr", ansFile, id),
"-v", fmt.Sprintf("%s:/woj/user/%d.judge", jdgFile, id),
"-e", fmt.Sprintf("TEST_NUM=%d", id),
"-e", fmt.Sprintf("CMP=%s", c.Cmp),
"git.0x7f.app/woj/ubuntu-full:latest",
"sh", "-c",
fmt.Sprintf("cd /woj/user && make -f %s judge", s.getLangScript(&c, lang)),
}
runArgs := &podmanArgs{
executeArgs: executeArgs{
args: args,
},
}
return s.podmanRun(runArgs)
}).
Done()
if err != nil {
s.log.Info("judge failed",
zap.Error(err),
zap.Uint("version", version),
zap.String("user", user),
zap.String("lang", lang),
)
}
}
}(task.Id)
id := s.pool.AddTask(f)
ids = append(ids, id)
}
for _, id := range ids {
s.pool.WaitForTask(id)
}
}
func (s *service) RunAndJudge(version uint, user string, lang string) (JudgeStatus, int32, e.Status) {
// 1. ensure problem/user exists
status := s.check(version, user, lang)
if status != e.Success {
return JudgeStatus{Message: "check failed"}, 0, status
}
// 2. config
config, err := s.ParseConfig(version, false)
if err != nil {
return JudgeStatus{Message: "parse config failed"}, 0, e.RunnerProblemParseFailed
}
// 3. run user program
s.problemRun(version, user, lang, &config)
// 4. run judger
s.problemJudge(version, user, lang, &config)
// 5. check result
result, pts := s.checkResults(user, &config)
return result, pts, e.Success
}

View File

@ -22,7 +22,7 @@ type Service interface {
// Compile compile user submission
Compile(version uint, user string, lang string) (JudgeStatus, e.Status)
// RunAndJudge execute user program
RunAndJudge(version uint, user string, lang string, config *Config) (JudgeStatus, int32, e.Status)
RunAndJudge(version uint, user string, lang string) (JudgeStatus, int32, e.Status)
// ParseConfig parse config file
ParseConfig(version uint, skipCheck bool) (Config, error)
@ -30,16 +30,20 @@ type Service interface {
ProblemExists(version uint) bool
HealthCheck() error
Shutdown() error
}
func NewService(i *do.Injector) (Service, error) {
concurrency := utils.If(runtime.NumCPU() > 1, runtime.NumCPU()-1, 1)
return &service{
srv := &service{
log: do.MustInvoke[log.Service](i).GetLogger("runner"),
pool: pool.NewTaskPool(concurrency, concurrency),
verbose: do.MustInvoke[config.Service](i).GetConfig().Development,
}, nil
}
srv.pool.Start()
return srv, nil
}
type service struct {
@ -51,3 +55,8 @@ type service struct {
func (s *service) HealthCheck() error {
return nil
}
func (s *service) Shutdown() error {
s.pool.Stop()
return nil
}

View File

@ -15,6 +15,11 @@ func (c *MustChain) Do(callback func() error) *MustChain {
return c
}
func (c *MustChain) DoAny(callback func() error) *MustChain {
_ = callback()
return c
}
func (c *MustChain) Done() error {
return c.err
}

View File

@ -1,21 +0,0 @@
#!/usr/bin/env bash
SCRIPT_PATH=$(cd "$(dirname "$0")" && pwd)
. "$SCRIPT_PATH/common.sh"
function docker_run() {
local timeout=${TIMEOUT:-10}
local memory=${MEMORY:-"256m"}
local log_file=${LOG_FILE:-"/dev/stderr"}
local log_limit=${LOG_LIMIT:-4K}
log_info "$DOCKER run with timeout $timeout"
CONTAINER_NAME=$(uuidgen)
(
sleep "$timeout"
$DOCKER kill "$CONTAINER_NAME"
) &
$DOCKER run --rm --name "$CONTAINER_NAME" --memory "$memory" "$@" 2>&1 | head -c "$log_limit" >"$log_file"
pkill -P $$
$DOCKER kill "$CONTAINER_NAME" >/dev/null 2>&1
return 0
}

View File

@ -1,76 +0,0 @@
#!/usr/bin/env bash
SCRIPT_PATH=$(cd "$(dirname "$0")" && pwd)
. "$SCRIPT_PATH/common.sh"
# get_problem_info
# extract language info and limits
# $1: workspace
# $2: problem name
# $3: language
# exports: Info_Script, Info_Cmp, Info_Num, Info_Limit_Time, Info_Limit_Memory, Info_Limit_NProc
function get_problem_info() {
local err
if [ ! -f "$1/problem/$2/config.json" ]; then
log_error "problem $2 not found"
return 1
fi
parse_language_info "$1" "$2" "$3"
err=$?
if [ "$err" -ne 0 ]; then
return "$err"
fi
parse_limits "$1" "$2"
err=$?
if [ "$err" -ne 0 ]; then
return "$err"
fi
}
function parse_language_info() {
export Info_Script
export Info_Cmp
local lang_config
local lang_type
local lang_script
lang_config=$(jq ".Languages[] | select(.Lang == \"$3\")" "$1/problem/$2/config.json")
if [ -z "$lang_config" ]; then
log_error "language $3 is not supported"
return 1
fi
Info_Cmp=$(echo "$lang_config" | jq -r ".Cmp")
lang_type=$(echo "$lang_config" | jq -r ".Type")
lang_script=$(echo "$lang_config" | jq -r ".Script")
if [ "$lang_type" == "custom" ]; then
Info_Script="/woj/problem/judge/$lang_script"
elif [ "$lang_type" == "default" ]; then
Info_Script="/woj/framework/template/default/$3.Makefile"
else
log_warn "Config file might be corrupted!"
log_error "Unknown language type: $lang_type"
return 1
fi
}
function parse_limits() {
export Info_Limit_Time
export Info_Limit_Memory
export Info_Limit_NProc
export Info_Num
local cfg
cfg="$1/problem/$2/config.json"
Info_Limit_Time=$(jq ".Runtime.TimeLimit" "$cfg")
Info_Limit_Memory=$(jq ".Runtime.MemoryLimit" "$cfg")
Info_Limit_NProc=$(jq ".Runtime.NProcLimit" "$cfg")
Info_Num=$(jq ".Tasks | length" "$1/problem/$2/config.json")
}

View File

@ -1,40 +0,0 @@
#!/usr/bin/env bash
WORKSPACE=$(cd "$(dirname "$0")"/.. && pwd)
. "$WORKSPACE"/scripts/common.sh
. "$WORKSPACE"/scripts/docker_run.sh
. "$WORKSPACE"/scripts/problem.sh
if [ "$1" == "" ] || [ ! -d "$WORKSPACE/problem/$1" ] || [ "$2" == "" ] || [ ! -d "$WORKSPACE/user/$2" ] || [ -z "$3" ]; then
log_warn "Usage: $0 <problem> <user_dir> <language> <timeout>"
exit 1
fi
get_problem_info "$WORKSPACE" "$1" "$3"
export TIMEOUT=${4:-60}
for test_num in $(seq "$Info_Num"); do
std_file="$WORKSPACE/problem/$1/data/output/$test_num.output"
ans_file="$WORKSPACE/user/$2/$test_num.out.usr"
jdg_file="$WORKSPACE/user/$2/$test_num.judge"
if [ ! -f "$std_file" ] || [ ! -f "$ans_file" ]; then
log_error "Missing test case $test_num"
exit 1
fi
log_info "Judging test case $test_num"
touch "$jdg_file"
docker_run \
-v "$WORKSPACE"/problem/"$1"/judge:/woj/problem/judge:ro \
-v "$WORKSPACE"/problem/"$1"/data:/woj/problem/data:ro \
-v "$ans_file":/woj/problem/user/"$test_num".out.usr \
-v "$jdg_file":/woj/problem/user/"$test_num".judge \
-e TEST_NUM="$test_num" \
-e CMP="$Info_Cmp" \
git.0x7f.app/woj/ubuntu-full \
sh -c \
"cd /woj/problem/user && make -f $Info_Script judge"
done

View File

@ -1,72 +0,0 @@
#!/usr/bin/env bash
WORKSPACE=$(cd "$(dirname "$0")"/.. && pwd)
. "$WORKSPACE"/scripts/common.sh
. "$WORKSPACE"/scripts/docker_run.sh
. "$WORKSPACE"/scripts/problem.sh
if [ "$1" == "" ] || [ ! -d "$WORKSPACE/problem/$1" ] || [ "$2" == "" ] || [ ! -d "$WORKSPACE/user/$2" ] || [ -z "$3" ]; then
log_warn "Usage: $0 <problem> <user_dir> <language>"
exit 1
fi
if [ ! -f "$WORKSPACE/problem/$1/.mark.prebuild" ]; then
log_warn "Problem $1 has not been prebuilt"
log_warn "Please run 'problem_prebuild.sh $1' first"
exit 1
fi
if [ ! -f "$WORKSPACE/user/$2/$2.out" ]; then
log_warn "User $2 has not been compiled"
log_warn "Please run 'problem_compile.sh ...' first"
exit 1
fi
parse_limits "$WORKSPACE" "$1"
log_info "Running problem $1 for user $2"
log_info "TimeLimit: $Info_Limit_Time"
log_info "MemoryLimit: $Info_Limit_Memory"
log_info "NProcLimit: $Info_Limit_NProc"
# launcher will add 2 more seconds
# here add 3 more seconds
TIMEOUT=$(((LIMIT_TIME + 1000) / 1000 + 4))
log_info "Timeout: $TIMEOUT"
for test_num in $(seq "$Info_Num"); do
test_case="$WORKSPACE/problem/$1/data/input/$test_num.input"
exe_file="$WORKSPACE/user/$2/$2.out"
ans_file="$WORKSPACE/user/$2/$test_num.out.usr"
ifo_file="$WORKSPACE/user/$2/$test_num.info"
if [ ! -f "$test_case" ]; then
log_error "Test case $test_num does not exist"
exit 1
fi
log_info "Running test case $test_num"
rm -f "$ans_file" && touch "$ans_file"
rm -f "$ifo_file" && touch "$ifo_file"
docker_run \
--cpus 1 \
--network none \
-v "$test_case":/woj/problem/data/input/"$test_num".input:ro \
-v "$exe_file":/woj/user/"$2".out:ro \
-v "$ans_file":/woj/user/"$test_num".out.usr \
-v "$ifo_file":/woj/user/"$test_num".info \
git.0x7f.app/woj/ubuntu-run \
sh -c \
"cd /woj/user && /woj/framework/scripts/woj_launcher \
--memory_limit=$Info_Limit_Memory \
--nproc_limit=$Info_Limit_NProc \
--time_limit=$Info_Limit_Time \
--sandbox_template=$3 \
--sandbox_action=ret \
--uid=1000 \
--gid=1000 \
--file_input=/woj/problem/data/input/$test_num.input \
--file_output=/woj/user/$test_num.out.usr \
--file_info=/woj/user/$test_num.info \
--program=/woj/user/$2.out"
done