From 78e5fc8d2e5d591cf86c8e8d6906e782161cde90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ron=20Nosz=C3=A1ly?= Date: Tue, 9 Apr 2024 19:47:33 +0200 Subject: [PATCH] Migrate new judge component from judge2 package. --- cmd/glue.go | 2 +- cmd/judge.go | 2 +- internal/glue/glue.go | 2 +- internal/glue/glue_test.go | 4 +- internal/judge/Dockerfile | 20 +- internal/judge/callback.go | 95 ----- internal/judge/client.go | 159 -------- internal/{judge2 => judge}/judge.go | 0 internal/{judge2 => judge}/judge_test.go | 0 internal/judge/queue.go | 112 ----- internal/judge/server.go | 386 +++++++----------- internal/judge/server_internal_test.go | 88 ---- internal/judge/status.go | 35 -- .../testdata/aplusb/problem.yaml | 0 .../testdata/aplusb/tests/1.in | 0 .../testdata/aplusb/tests/1.out | 0 .../testdata/aplusb/tests/2.in | 0 .../testdata/aplusb/tests/2.out | 0 .../testdata/aplusb/tests/3.in | 0 .../testdata/aplusb/tests/3.out | 0 internal/judge/worker.go | 174 -------- internal/judge/worker_test.go | 117 ------ internal/judge2/server.go | 194 --------- 23 files changed, 155 insertions(+), 1235 deletions(-) delete mode 100644 internal/judge/callback.go delete mode 100644 internal/judge/client.go rename internal/{judge2 => judge}/judge.go (100%) rename internal/{judge2 => judge}/judge_test.go (100%) delete mode 100644 internal/judge/queue.go delete mode 100644 internal/judge/server_internal_test.go delete mode 100644 internal/judge/status.go rename internal/{judge2 => judge}/testdata/aplusb/problem.yaml (100%) rename internal/{judge2 => judge}/testdata/aplusb/tests/1.in (100%) rename internal/{judge2 => judge}/testdata/aplusb/tests/1.out (100%) rename internal/{judge2 => judge}/testdata/aplusb/tests/2.in (100%) rename internal/{judge2 => judge}/testdata/aplusb/tests/2.out (100%) rename internal/{judge2 => judge}/testdata/aplusb/tests/3.in (100%) rename internal/{judge2 => judge}/testdata/aplusb/tests/3.out (100%) delete mode 100644 internal/judge/worker.go delete mode 100644 internal/judge/worker_test.go delete mode 100644 internal/judge2/server.go diff --git a/cmd/glue.go b/cmd/glue.go index f310b8a1..5c40bb20 100644 --- a/cmd/glue.go +++ b/cmd/glue.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" "github.com/mraron/njudge/internal/glue" - "github.com/mraron/njudge/internal/judge2" + "github.com/mraron/njudge/internal/judge" "github.com/mraron/njudge/internal/web/helpers/config" "github.com/spf13/cobra" "github.com/spf13/pflag" diff --git a/cmd/judge.go b/cmd/judge.go index 9d4a569b..49dfdd43 100644 --- a/cmd/judge.go +++ b/cmd/judge.go @@ -3,7 +3,7 @@ package cmd import ( "errors" "fmt" - "github.com/mraron/njudge/internal/judge2" + "github.com/mraron/njudge/internal/judge" "github.com/mraron/njudge/pkg/language" "github.com/mraron/njudge/pkg/language/sandbox" "github.com/mraron/njudge/pkg/problems" diff --git a/internal/glue/glue.go b/internal/glue/glue.go index 5195e22d..97f61d66 100644 --- a/internal/glue/glue.go +++ b/internal/glue/glue.go @@ -3,7 +3,7 @@ package glue import ( "context" "fmt" - "github.com/mraron/njudge/internal/judge2" + "github.com/mraron/njudge/internal/judge" "github.com/mraron/njudge/internal/njudge/db" "github.com/mraron/njudge/internal/web/helpers/config" "strconv" diff --git a/internal/glue/glue_test.go b/internal/glue/glue_test.go index 6f8eb512..1716d9f4 100644 --- a/internal/glue/glue_test.go +++ b/internal/glue/glue_test.go @@ -4,7 +4,7 @@ import ( "context" "errors" "github.com/mraron/njudge/internal/glue" - "github.com/mraron/njudge/internal/judge2" + "github.com/mraron/njudge/internal/judge" "github.com/mraron/njudge/internal/njudge" "github.com/mraron/njudge/internal/njudge/memory" "github.com/mraron/njudge/pkg/language" @@ -216,7 +216,7 @@ func TestGlue_ProcessSubmission(t *testing.T) { func TestJudgeIntegration(t *testing.T) { s1, _ := sandbox.NewDummy() s2, _ := sandbox.NewDummy() - store := problems.NewFsStore("../judge2/testdata") + store := problems.NewFsStore("../judge/testdata") _ = store.UpdateProblems() judge := &judge2.Judge{ diff --git a/internal/judge/Dockerfile b/internal/judge/Dockerfile index d4a119e5..9ae8afff 100644 --- a/internal/judge/Dockerfile +++ b/internal/judge/Dockerfile @@ -3,31 +3,19 @@ ARG PROJECT_NAME FROM ubuntu:22.04 as judge_deps RUN apt-get update && apt-get install -y wget gcc g++ git build-essential libcap-dev WORKDIR /app -RUN git clone https://github.com/ioi/isolate.git +RUN git clone --depth 1 --branch v1.10.1 https://github.com/ioi/isolate.git WORKDIR /app/isolate RUN make isolate -WORKDIR / -RUN mkdir languages - -WORKDIR /languages -RUN wget https://julialang-s3.julialang.org/bin/linux/x64/1.8/julia-1.8.4-linux-x86_64.tar.gz && \ - tar zxvf julia-1.8.4-linux-x86_64.tar.gz && \ - rm julia-1.8.4-linux-x86_64.tar.gz - -RUN wget https://nim-lang.org/download/nim-1.6.10-linux_x64.tar.xz && \ - tar xvf nim-1.6.10-linux_x64.tar.xz && \ - rm nim-1.6.10-linux_x64.tar.xz - FROM ${PROJECT_NAME}-base COPY --from=judge_deps /app/isolate /app/isolate WORKDIR /app/isolate RUN make install -COPY --from=judge_deps /languages/* /languages/ -RUN ln -s /languages/julia-1.8.4/bin/julia /usr/local/bin/julia && \ - ln -s /languages/nim-1.6.10/bin/nim /usr/bin/nim +COPY --from=julia:1.10.1 /usr/local/julia/ /usr/local/julia +COPY --from=nimlang/nim:1.6.18 /usr/bin/nim /usr/bin/nim +RUN ln -s /usr/local/julia/bin/julia /usr/local/bin/julia WORKDIR /app COPY configs/docker/judge_docker.json ./judge.json diff --git a/internal/judge/callback.go b/internal/judge/callback.go deleted file mode 100644 index 23d4f91b..00000000 --- a/internal/judge/callback.go +++ /dev/null @@ -1,95 +0,0 @@ -package judge - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "time" -) - -type SubmissionStatus struct { - Response - Time time.Time -} - -type Callbacker interface { - Callback(Response) error -} - -type ChanCallback struct { - c chan Response -} - -func NewChanCallback(c chan Response) *ChanCallback { - return &ChanCallback{c} -} - -func (cc *ChanCallback) Callback(r Response) error { - cc.c <- r - if (r.Done) { - close(cc.c) - } - - return nil -} - -type WriterCallback struct { - enc *json.Encoder - err error - afterFunc func() -} - -func NewWriterCallback(w io.Writer, afterFunc func()) *WriterCallback { - return &WriterCallback{enc: json.NewEncoder(w), err: nil, afterFunc: afterFunc} -} - -func (wc *WriterCallback) Callback(r Response) error { - if wc.err != nil { - return wc.err - } - - wc.err = wc.enc.Encode(SubmissionStatus{Response: r, Time: time.Now()}) - if wc.err == nil { - wc.afterFunc() - } - - return wc.err -} - -func (wc *WriterCallback) Error() error { - return wc.err -} - -type HTTPCallback struct { - url string -} - -func NewHTTPCallback(url string) HTTPCallback { - return HTTPCallback{url} -} - -func (h HTTPCallback) Callback(r Response) error { - raw := SubmissionStatus{Response: r, Time: time.Now()} - - buf := &bytes.Buffer{} - - data := json.NewEncoder(buf) - err := data.Encode(raw) - if err != nil { - return err - } - - resp, err := http.Post(h.url, "application/json", buf) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("Callback error: %s %s", resp.Status, resp.Body) - } - - return nil -} diff --git a/internal/judge/client.go b/internal/judge/client.go deleted file mode 100644 index 96ed2d40..00000000 --- a/internal/judge/client.go +++ /dev/null @@ -1,159 +0,0 @@ -package judge - -import ( - "bufio" - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" -) - -type Submission struct { - Id string `json:"id"` - Problem string `json:"problem"` - Language string `json:"language"` - Source []byte `json:"source"` - - Stream bool `json:"stream"` - CallbackUrl string `json:"callback_url"` -} - -type Client struct { - client *http.Client - - url string - token string -} - -type ClientOption func(*Client) - -func NewClient(url string, opts ...ClientOption) *Client { - client := &Client{url: url, client: &http.Client{}} - for i := range opts { - opts[i](client) - } - - return client -} - -func (dc Client) submit(ctx context.Context, sub Submission) (*http.Response, error) { - dst := dc.url + "/judge" - - buf := bytes.Buffer{} - - enc := json.NewEncoder(&buf) - err := enc.Encode(sub) - if err != nil { - return nil, err - } - - req, err := http.NewRequestWithContext(ctx, "POST", dst, &buf) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", "application/json") - if dc.token != "" { - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", dc.token)) - } - - resp, err := dc.client.Do(req) - if err != nil { - return nil, err - } - - return resp, nil -} - -func (dc Client) SubmitCallback(ctx context.Context, sub Submission, callback string) error { - sub.Stream = false - sub.CallbackUrl = callback - resp, err := dc.submit(ctx, sub) - if err != nil { - return err - } - - defer resp.Body.Close() - - data, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - - if string(data) != "queued" { - return errors.New(string(data)) - } - - return nil -} - -func (dc Client) SubmitStream(ctx context.Context, sub Submission, res chan SubmissionStatus) error { - var err error - - sub.Stream = true - sub.CallbackUrl = "" - resp, err := dc.submit(ctx, sub) - if err != nil { - return err - } - - done := make(chan bool, 1) - - go func() { - s := bufio.NewScanner(resp.Body) - - for s.Scan() { - status := SubmissionStatus{} - if err = json.Unmarshal([]byte(s.Text()), &status); err != nil { - return - } - - res <- status - } - - err = resp.Body.Close() - done <- true - }() - - select { - case <-done: - return err - case <-ctx.Done(): - return ctx.Err() - } -} - -func (dc Client) Status(ctx context.Context) (ServerStatus, error) { - dst := dc.url + "/status" - - req, err := http.NewRequestWithContext(ctx, "GET", dst, nil) - if err != nil { - return ServerStatus{}, err - } - - if dc.token != "" { - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", dc.token)) - } - - resp, err := dc.client.Do(req) - if err != nil { - return ServerStatus{}, err - } - - ans := ServerStatus{} - - dec := json.NewDecoder(resp.Body) - err = dec.Decode(&ans) - if err != nil { - return ServerStatus{}, err - } - - if resp.StatusCode != http.StatusOK { - return ServerStatus{}, errors.New("judger returned: " + resp.Status) - } - - return ans, nil -} diff --git a/internal/judge2/judge.go b/internal/judge/judge.go similarity index 100% rename from internal/judge2/judge.go rename to internal/judge/judge.go diff --git a/internal/judge2/judge_test.go b/internal/judge/judge_test.go similarity index 100% rename from internal/judge2/judge_test.go rename to internal/judge/judge_test.go diff --git a/internal/judge/queue.go b/internal/judge/queue.go deleted file mode 100644 index 570b5db7..00000000 --- a/internal/judge/queue.go +++ /dev/null @@ -1,112 +0,0 @@ -package judge - -import ( - "context" - "errors" - "fmt" - - "github.com/mraron/njudge/pkg/language" - "github.com/mraron/njudge/pkg/problems" - "go.uber.org/zap" -) - -type Enqueuer interface { - Enqueue(context.Context, Submission) (<-chan Response, error) - - SupportedProblems() ([]string, error) - SupportedLanguages() ([]string, error) -} - -type Response struct { - Test string - Status problems.Status - Done bool - Error string -} - -type queueSubmission struct { - Submission - c Callbacker -} - -type Queue struct { - problemStore problems.Store - languageStore language.Store - workerProvider WorkerProvider - - queue chan queueSubmission - - logger *zap.Logger -} - -func NewQueue(logger *zap.Logger, problemStore problems.Store, languageStore language.Store, workerProvider WorkerProvider) (*Queue, error) { - queue := &Queue{ - problemStore: problemStore, - languageStore: languageStore, - workerProvider: workerProvider, - queue: make(chan queueSubmission, 128), - logger: logger, - } - - return queue, nil -} - -func (j *Queue) Enqueue(ctx context.Context, sub Submission) (<-chan Response, error) { - channel := make(chan Response) - - qs := queueSubmission{Submission: sub} - qs.c = NewChanCallback(channel) - j.queue <- qs - - return channel, nil -} - -func (q *Queue) SupportedProblems() ([]string, error) { - return q.problemStore.ListProblems() -} - -func (q *Queue) SupportedLanguages() ([]string, error) { - res := []string{} - for _, val := range q.languageStore.List() { - res = append(res, val.ID()) - } - - return res, nil -} - -func (j *Queue) Run() { - judge := func(worker *Worker, sub queueSubmission) error { - p, err := j.problemStore.GetProblem(sub.Problem) - if err != nil { - return err - } - - lang, _ := j.languageStore.Get(sub.Language) - if lang == nil { - return fmt.Errorf("no such language: %s", sub.Language) - } - - st, err := worker.Judge(context.Background(), j.logger, p, sub.Source, lang, sub.c) - if err != nil { - j.logger.Error("judge error", zap.Error(err)) - - st.Compiled = false - st.CompilerOutput = "internal error: " + err.Error() - return errors.Join(sub.c.Callback(Response{"", st, true, err.Error()}), err) - } else { - return sub.c.Callback(Response{"", st, true, ""}) - } - } - - for sub := range j.queue { - sub := sub - go func() { - w := j.workerProvider.Get() - if err := judge(w, sub); err != nil { - j.logger.Error("judging error", zap.Error(err)) - } - - j.workerProvider.Put(w) - }() - } -} diff --git a/internal/judge/server.go b/internal/judge/server.go index 4f067df1..c57ef107 100644 --- a/internal/judge/server.go +++ b/internal/judge/server.go @@ -1,288 +1,194 @@ -package judge +package judge2 import ( - "context" + "encoding/json" "errors" "fmt" - "log" - "net/http" - "strconv" - "strings" - "sync" - "time" - "github.com/labstack/echo/v4" - "github.com/shirou/gopsutil/load" - "go.uber.org/zap" - + "github.com/labstack/echo/v4/middleware" "github.com/mraron/njudge/pkg/language" "github.com/mraron/njudge/pkg/problems" _ "github.com/mraron/njudge/pkg/problems/config/feladat_txt" _ "github.com/mraron/njudge/pkg/problems/config/polygon" - _ "github.com/mraron/njudge/pkg/problems/evaluation/batch" - _ "github.com/mraron/njudge/pkg/problems/evaluation/communication" - _ "github.com/mraron/njudge/pkg/problems/evaluation/stub" - - "encoding/json" + _ "github.com/mraron/njudge/pkg/problems/config/problem_yaml" + _ "github.com/mraron/njudge/pkg/problems/config/task_yaml" + slogecho "github.com/samber/slog-echo" + "log/slog" + "net/http" + "time" - "github.com/labstack/echo/v4/middleware" - _ "github.com/mraron/njudge/pkg/language/langs/cpp" - _ "github.com/mraron/njudge/pkg/language/langs/csharp" - _ "github.com/mraron/njudge/pkg/language/langs/golang" - _ "github.com/mraron/njudge/pkg/language/langs/java" - _ "github.com/mraron/njudge/pkg/language/langs/julia" - _ "github.com/mraron/njudge/pkg/language/langs/nim" - _ "github.com/mraron/njudge/pkg/language/langs/pascal" - _ "github.com/mraron/njudge/pkg/language/langs/pypy3" _ "github.com/mraron/njudge/pkg/language/langs/python3" - _ "github.com/mraron/njudge/pkg/language/langs/zip" ) -type ServerConfig struct { - HTTPConfig `mapstructure:",squash"` - SandboxIds string `json:"sandbox_ids" mapstructure:"sandbox_ids"` - WorkerCount int `json:"worker_count" mapstructure:"worker_count"` - ProblemsDir string `json:"problems_dir" mapstructure:"problems_dir"` - Mode string `json:"mode" mapstructure:"mode"` +type Submission struct { + ID string `json:"id"` + Problem string `json:"problem"` + Language string `json:"language"` + Source []byte `json:"source"` + //TODO add status skeleton here? } -type Server struct { - ServerConfig +type Result struct { + Index int `json:"index,omitempty"` + Test string `json:"test,omitempty"` + Status *problems.Status `json:"status"` + Error string `json:"error,omitempty"` +} - problemStore problems.Store - httpServer *HTTPServer +type Server struct { + Logger *slog.Logger + Judger Judger + ProblemStore problems.Store - queue *Queue - logger *zap.Logger + Config struct { + Port string + } } -func NewServer(cfg ServerConfig) (*Server, error) { - s := &Server{ServerConfig: cfg} +type ServerOption func(*Server) - var err error - if s.Mode == "development" { - s.logger, err = zap.NewDevelopment() - } else { - s.logger, err = zap.NewProduction() +func WithPortServerOption(port int) ServerOption { + return func(server *Server) { + server.Config.Port = fmt.Sprintf("%d", port) } +} - if err != nil { - return nil, err +func NewServer(logger *slog.Logger, judger Judger, problemStore problems.Store, opts ...ServerOption) Server { + res := Server{Logger: logger, Judger: judger, ProblemStore: problemStore} + res.Config.Port = "8080" + for _, opt := range opts { + opt(&res) } + return res +} - minSandboxId, maxSandboxId := -1, -1 - if s.SandboxIds == "" { - minSandboxId = 100 - maxSandboxId = 999 - } else { - splitted := strings.Split(s.SandboxIds, "-") - if len(splitted) != 2 { - return nil, fmt.Errorf("sandbox_ids wrong format") +func (s Server) PostJudgeHandler() echo.HandlerFunc { + return func(c echo.Context) error { + sub := Submission{} + if err := c.Bind(&sub); err != nil { + return err } - var err1, err2 error - minSandboxId, err1 = strconv.Atoi(splitted[0]) - maxSandboxId, err2 = strconv.Atoi(splitted[1]) - if err1 != nil || err2 != nil { - return nil, errors.Join(err1, err2) + inited := false + initResponse := func(statusCode int) { + if inited { + return + } + inited = true + c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + c.Response().WriteHeader(statusCode) } - } - - s.logger.Info("initializing workers") - wp, err := NewIsolateWorkerProvider(minSandboxId, maxSandboxId, cfg.WorkerCount) - if err != nil { - return nil, err - } - - s.problemStore = problems.NewFsStore(cfg.ProblemsDir) - if err = s.problemStore.UpdateProblems(); err != nil { - s.logger.Info("failed to initialize problems", zap.Error(err)) - } + enc := json.NewEncoder(c.Response().Writer) - ls := language.DefaultStore - - s.logger.Info("initializing the queue") - s.queue, err = NewQueue(s.logger, s.problemStore, ls, wp) - if err != nil { - return nil, err - } - - s.logger.Info("initializing the http server") - s.httpServer = NewHTTPServer(s.HTTPConfig, s.queue, s.logger) - - return s, nil -} - -func (s *Server) Run() { - go func() { - for { - if err := s.problemStore.UpdateProblems(); err != nil { - s.logger.Error("updating problems", zap.Error(err)) + st, err := s.Judger.Judge(c.Request().Context(), sub, func(result Result) error { + initResponse(http.StatusOK) + return enc.Encode(result) + }) + res := Result{ + Status: st, + } + if err != nil { + res.Error = err.Error() + if errors.Is(err, problems.ErrorProblemNotFound) || errors.Is(err, language.ErrorLanguageNotFound) { + initResponse(http.StatusBadRequest) + return enc.Encode(res) + } + if st == nil { + return err } - - time.Sleep(20 * time.Second) } - }() - - s.logger.Info("starting the queue") - go s.queue.Run() - - s.logger.Info("starting the http server") - s.httpServer.Run() -} - -type HTTPConfig struct { - Host string `json:"host" mapstructure:"host"` - Port string `json:"port" mapstructure:"port"` -} - -type HTTPServer struct { - HTTPConfig - Enqueuer - - status ServerStatus - statusMutex sync.RWMutex - - start time.Time - logger *zap.Logger -} - -func NewHTTPServer(cfg HTTPConfig, j Enqueuer, logger *zap.Logger) *HTTPServer { - s := HTTPServer{HTTPConfig: cfg, Enqueuer: j, logger: logger} - s.start = time.Now() - - return &s -} - -func (s *HTTPServer) Run() error { - if err := s.init(); err != nil { - return err + initResponse(http.StatusOK) + return enc.Encode(res) } - - e := echo.New() - e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogMethod: true, - LogURI: true, - LogStatus: true, - LogHost: true, - LogRemoteIP: true, - LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { - s.logger.Info("request", - zap.String("method", v.Method), - zap.String("URI", v.URI), - zap.String("host", v.Host), - zap.String("remoteip", v.RemoteIP), - zap.Int("status", v.Status), - ) - - return nil - }, - })) - - e.GET("/status", s.getStatus) - e.POST("/judge", s.postJudge) - - go s.runUpdate() - - return e.Start(":" + s.Port) -} - -func (s *HTTPServer) init() error { - s.status.Host = s.Host - s.status.Port = s.Port - s.status.Url = "http://" + s.Host + ":" + s.Port - - return nil } -func (s *HTTPServer) runUpdate() { +func (s Server) Run() error { go func() { for { - l, err := load.Avg() - - if err != nil { - log.Print("Error while getting load: ", err) - } else { - s.statusMutex.Lock() - s.status.Load = l.Load1 - s.statusMutex.Unlock() + if err := s.ProblemStore.UpdateProblems(); err != nil { + s.Logger.Error("failed to update problemStore", err) } - - time.Sleep(60 * time.Second) + time.Sleep(30 * time.Second) } }() - go func() { - for { - s.statusMutex.Lock() - s.status.LanguageList, _ = s.Enqueuer.SupportedLanguages() - s.status.ProblemList, _ = s.Enqueuer.SupportedProblems() - s.statusMutex.Unlock() - - time.Sleep(20 * time.Second) - } - }() + e := echo.New() + e.Use(slogecho.New(s.Logger)) + e.Use(middleware.Recover()) - for { - s.statusMutex.Lock() - s.status.Uptime = time.Since(s.start) - s.statusMutex.Unlock() - time.Sleep(1 * time.Second) - } -} + e.POST("/judge", s.PostJudgeHandler()) -func (s *HTTPServer) getStatus(c echo.Context) error { - s.statusMutex.RLock() - defer s.statusMutex.RUnlock() - return c.JSON(http.StatusOK, s.status) + return e.Start(":" + s.Config.Port) } -func (s *HTTPServer) postJudge(c echo.Context) error { - sub := Submission{} - if err := c.Bind(&sub); err != nil { - log.Print("getJudge error binding:", err) - return c.String(http.StatusBadRequest, "Parse error") +/* +func main() { + s1, _ := sandbox.NewIsolate(104) + s2, _ := sandbox.NewIsolate(105) + provider := sandbox.NewProvider().Put(s1).Put(s2) + + problemStore := problems.NewFsStore("/home/aron/Projects/njudge/njudge_problems_git") + _ = problemStore.UpdateProblems() + languageStore := language.DefaultStore + + judge := Judge{ + SandboxProvider: provider, + ProblemStore: problemStore, + LanguageStore: languageStore, + RateLimit: 5 * time.Second, } - if sub.Stream { - c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - c.Response().WriteHeader(http.StatusOK) - - callback := NewWriterCallback(c.Response(), func() { - c.Response().Flush() - }) - - res, err := s.Enqueue(c.Request().Context(), sub) - if err != nil { - return err - } - for resp := range res { - if err := callback.Callback(resp); err != nil { - return err - } - } - - return nil - } else { - callback := NewHTTPCallback(sub.CallbackUrl) - res, err := s.Enqueue(context.Background(), sub) - if err != nil { - return err - } + server := NewServer(slog.Default(), &judge, problemStore, WithPortServerOption(8081)) + go func() { + fmt.Println("start sleep") + time.Sleep(5 * time.Second) + ctx, cancel := context.WithCancel(context.Background()) go func() { - for resp := range res { - err := callback.Callback(resp) - if err != nil { - s.logger.Error("error calling back", zap.Error(err)) - } - } + time.Sleep(10 * time.Second) + cancel() }() - - return c.String(http.StatusOK, "queued") - } -} - -func (s *HTTPServer) ToString() (string, error) { - val, err := json.Marshal(s) - return string(val), err + client := NewClient("http://localhost:8081") + res, err := client.Judge(ctx, Submission{ + Problem: "KK24_csoki2", + Language: "python3", + Source: []byte(`while True: pass`), + }, func(result Result) error { + for _, tc := range result.Status.Feedback[0].Testcases() { + fmt.Print(tc.VerdictName) + } + fmt.Println("") + return nil + }) + fmt.Println("ends: ", res, err) + }() + panic(server.Run()) + /* + fmt.Println(judge.Judge(context.Background(), Submission{ + ID: "", + Problem: "KK24_csoki22", + Language: "python3", + Source: []byte(`// @check-accepted: examples N=0 no-limits + + #include + #include + #include + + using namespace std; + + int main() { + int M, N, K; + + cin >> M >> N >> K; + + if ( (N+M)%K == 0 ) { + cout << "IGEN" << endl; + } else { + cout << "NEM" << endl; + } + + return 0; + } + `), + }, nil)) } +*/ diff --git a/internal/judge/server_internal_test.go b/internal/judge/server_internal_test.go deleted file mode 100644 index ba0d89c9..00000000 --- a/internal/judge/server_internal_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package judge - -import ( - "bufio" - "bytes" - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/labstack/echo/v4" - "go.uber.org/zap" -) - -type customEnqueuer struct { - ch chan Response -} - -func newCustomEnqueuer(r []Response) *customEnqueuer { - res := &customEnqueuer{make(chan Response, len(r))} - for i := range r { - res.ch <- r[i] - } - close(res.ch) - - return res -} - -func (c customEnqueuer) Enqueue(context.Context, Submission) (<-chan Response, error) { - return c.ch, nil -} - -func (c customEnqueuer) SupportedProblems() ([]string, error) { - return nil, nil -} - -func (c customEnqueuer) SupportedLanguages() ([]string, error) { - return nil, nil -} - - -func TestPostJudgeStream(t *testing.T) { - resps := []Response{ - {Test: "1"}, - {Test: "423"}, - {Test: "2", Done: true}, - } - - s := NewHTTPServer(HTTPConfig{"", ""}, newCustomEnqueuer(resps), zap.NewNop()) - - buf := bytes.Buffer{} - - if err := json.NewEncoder(&buf).Encode(Submission{Stream: true}); err != nil { - t.Error(err) - } - - e := echo.New() - req := httptest.NewRequest(http.MethodGet, "/", &buf) - req.Header.Set("Content-Type", "application/json") - - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - c.SetPath("/judge") - - if err := s.postJudge(c); err != nil { - t.Error(err) - } - - sc := bufio.NewScanner(rec.Body) - ind := 0 - for sc.Scan() { - status := SubmissionStatus{} - if err := json.Unmarshal([]byte(sc.Text()), &status); err != nil { - return - } - - if resps[ind].Test != status.Test { - t.Errorf("%s != %s", resps[ind].Test, status.Test) - } - - ind ++ - } - - if ind != len(resps) { - t.Error("wrong number") - } -} \ No newline at end of file diff --git a/internal/judge/status.go b/internal/judge/status.go deleted file mode 100644 index adad1bed..00000000 --- a/internal/judge/status.go +++ /dev/null @@ -1,35 +0,0 @@ -package judge - -import ( - "encoding/json" - "slices" - "time" -) - -type ServerStatus struct { - Host string `json:"host"` - Port string `json:"port"` - Url string `json:"url"` - Load float64 `json:"load"` - Uptime time.Duration `json:"uptime"` - ProblemList []string `json:"problem_list"` - LanguageList []string `json:"language_list"` -} - -func ParseServerStatus(s string) (res ServerStatus, err error) { - err = json.Unmarshal([]byte(s), &res) - return -} - -func (s ServerStatus) SupportsProblem(want string) bool { - return slices.Contains(s.ProblemList, want) -} - -func (s ServerStatus) SupportsLanguage(want string) bool { - return slices.Contains(s.LanguageList, want) -} - -func (s ServerStatus) String() string { - res, _ := json.Marshal(s) - return string(res) -} diff --git a/internal/judge2/testdata/aplusb/problem.yaml b/internal/judge/testdata/aplusb/problem.yaml similarity index 100% rename from internal/judge2/testdata/aplusb/problem.yaml rename to internal/judge/testdata/aplusb/problem.yaml diff --git a/internal/judge2/testdata/aplusb/tests/1.in b/internal/judge/testdata/aplusb/tests/1.in similarity index 100% rename from internal/judge2/testdata/aplusb/tests/1.in rename to internal/judge/testdata/aplusb/tests/1.in diff --git a/internal/judge2/testdata/aplusb/tests/1.out b/internal/judge/testdata/aplusb/tests/1.out similarity index 100% rename from internal/judge2/testdata/aplusb/tests/1.out rename to internal/judge/testdata/aplusb/tests/1.out diff --git a/internal/judge2/testdata/aplusb/tests/2.in b/internal/judge/testdata/aplusb/tests/2.in similarity index 100% rename from internal/judge2/testdata/aplusb/tests/2.in rename to internal/judge/testdata/aplusb/tests/2.in diff --git a/internal/judge2/testdata/aplusb/tests/2.out b/internal/judge/testdata/aplusb/tests/2.out similarity index 100% rename from internal/judge2/testdata/aplusb/tests/2.out rename to internal/judge/testdata/aplusb/tests/2.out diff --git a/internal/judge2/testdata/aplusb/tests/3.in b/internal/judge/testdata/aplusb/tests/3.in similarity index 100% rename from internal/judge2/testdata/aplusb/tests/3.in rename to internal/judge/testdata/aplusb/tests/3.in diff --git a/internal/judge2/testdata/aplusb/tests/3.out b/internal/judge/testdata/aplusb/tests/3.out similarity index 100% rename from internal/judge2/testdata/aplusb/tests/3.out rename to internal/judge/testdata/aplusb/tests/3.out diff --git a/internal/judge/worker.go b/internal/judge/worker.go deleted file mode 100644 index 237b1e16..00000000 --- a/internal/judge/worker.go +++ /dev/null @@ -1,174 +0,0 @@ -package judge - -import ( - "context" - "errors" - "fmt" - "github.com/mraron/njudge/pkg/problems/evaluation" - "io" - - "github.com/mraron/njudge/pkg/language" - "github.com/mraron/njudge/pkg/language/sandbox" - "github.com/mraron/njudge/pkg/problems" - "go.uber.org/zap" -) - -type Worker struct { - id int - sandboxProvider *sandbox.ChanProvider -} - -func NewWorker(id int, sandboxProvider *sandbox.ChanProvider) *Worker { - return &Worker{id: id, sandboxProvider: sandboxProvider} -} - -func (w Worker) Judge(ctx context.Context, plogger *zap.Logger, p problems.EvaluationInfo, src []byte, lang language.Language, c Callbacker) (st problems.Status, err error) { - logger := plogger.With(zap.Int("worker", w.id)) - logger.Info("started to judge") - - sandboxes := sandbox.NewProvider() - for i := 0; i < 2; i++ { - var s sandbox.Sandbox - s, err = w.sandboxProvider.Get() - if err != nil { - logger.Error("can't get sandbox", zap.Error(err)) - return - } - defer w.sandboxProvider.Put(s) - - err = s.Init(context.TODO()) - if err != nil { - return - } - sandboxes.Put(s) - - defer func(sandbox sandbox.Sandbox) { - err = errors.Join(err, sandbox.Cleanup(context.TODO())) - }(s) - } - - tt := p.GetTaskType() - - logger.Info("compiling") - compileSandbox, _ := sandboxes.Get() - compileRes, err := tt.Compile(context.Background(), evaluation.NewByteSolution(lang, src), compileSandbox) - sandboxes.Put(compileSandbox) - - if err != nil { - logger.Error("compilation error", zap.Error(err)) - st.Compiled = false - st.CompilerOutput = err.Error() + "\n" + truncate(compileRes.CompilationMessage, 1024) - - return st, nil - } - st.Compiled = true - - var ( - testNotifier = make(chan string) - statusNotifier = make(chan problems.Status) - errRun error - test string = "1" - - waiter = make(chan struct{}) - ) - - go func() { - skeleton, _ := p.StatusSkeleton("") - - bin, _ := io.ReadAll(compileRes.CompiledFile.Source) - - st, errRun = tt.Evaluate(context.Background(), *skeleton, evaluation.NewByteSolution(lang, bin), sandboxes, evaluation.IgnoreStatusUpdate{}) - close(testNotifier) - close(statusNotifier) - waiter <- struct{}{} - }() - - for status := range statusNotifier { - test = <-testNotifier - err = c.Callback(Response{test, status, false, ""}) - if err != nil { - logger.Error("error while calling callback", zap.Error(err)) - return - } - } - - <-waiter - err = errors.Join(err, errRun) - if err == nil { - logger.Info("successful judging") - } else { - logger.Info("got error while judging", zap.Error(err)) - } - - return -} - -type WorkerProvider interface { - Get() *Worker - Put(*Worker) -} - -type IsolateWorkerProvider struct { - minSandboxId, maxSandboxId int - sandboxIdUsed map[int]struct{} - workers chan *Worker - workerCount int -} - -func NewIsolateWorkerProvider(minSandboxId, maxSandboxId, workerCount int) (*IsolateWorkerProvider, error) { - wp := &IsolateWorkerProvider{ - minSandboxId: minSandboxId, - maxSandboxId: maxSandboxId, - workerCount: workerCount, - workers: make(chan *Worker, workerCount), - sandboxIdUsed: make(map[int]struct{}), - } - - for i := 0; i < wp.workerCount; i++ { - provider := sandbox.NewProvider() - if err := wp.populateProvider(provider, 2); err != nil { - return nil, err - } - - wp.workers <- NewWorker(i+1, provider) - } - - return wp, nil -} - -func (wp *IsolateWorkerProvider) Get() *Worker { - return <-wp.workers -} - -func (wp *IsolateWorkerProvider) Put(w *Worker) { - wp.workers <- w -} - -func (wp *IsolateWorkerProvider) populateProvider(provider *sandbox.ChanProvider, cnt int) error { - for i := wp.minSandboxId; i <= wp.maxSandboxId; i++ { - if _, ok := wp.sandboxIdUsed[i]; !ok { - s, _ := sandbox.NewIsolate(i) - provider.Put(s) - cnt -= 1 - wp.sandboxIdUsed[i] = struct{}{} - } - - if cnt == 0 { - break - } - } - - if cnt != 0 { - return fmt.Errorf("not enough sandboxes") - } - - return nil -} - -func truncate(s string, to int) string { - if len(s) < to { - return s - } - - return s[:to-1] + "..." -} diff --git a/internal/judge/worker_test.go b/internal/judge/worker_test.go deleted file mode 100644 index a225284b..00000000 --- a/internal/judge/worker_test.go +++ /dev/null @@ -1,117 +0,0 @@ -package judge_test - -import ( - "context" - "errors" - problemsMock "github.com/mraron/njudge/mocks/github.com/mraron/njudge/pkg/problems" - "github.com/stretchr/testify/mock" - "io" - "sync" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/mraron/njudge/internal/judge" - "github.com/mraron/njudge/pkg/language/sandbox" - "github.com/mraron/njudge/pkg/problems" - "go.uber.org/zap" -) - -func TestWorker(t *testing.T) { - tests := []struct { - Name string - Judgeable func() problems.EvaluationInfo - JudgeReturnStatus problems.Status - JudgeReturnErr error - Responses []judge.Response - }{ - { - "TestWorkerRunning", - func() problems.EvaluationInfo { - var tasktype problemsMock.TaskType - tasktype.On("Compile", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) - tasktype.On("Run", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - c1, c2 := args.Get(4).(chan string), args.Get(5).(chan problems.Status) - c2 <- problems.Status{CompilerOutput: "hehe"} - c1 <- "1" - c2 <- problems.Status{CompilerOutput: "huhu"} - c1 <- "2" - close(c1) - close(c2) - }).Return(problems.Status{}, nil) - - var judgeable problemsMock.Judgeable - judgeable.On("GetTaskType").Return(&tasktype) - return &judgeable - }, - problems.Status{}, - nil, - []judge.Response{ - {Test: "1", Status: problems.Status{CompilerOutput: "hehe"}}, - {Test: "2", Status: problems.Status{CompilerOutput: "huhu"}}, - }, - }, - { - "TestWorkerCompileError", - func() problems.EvaluationInfo { - var tasktype problemsMock.TaskType - tasktype.On("Compile", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - w := args.Get(4).(io.Writer) - w.Write([]byte("a")) - }).Return(nil, errors.New("")) - tasktype.On("Run", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(problems.Status{}, nil) - - var judgeable problemsMock.Judgeable - judgeable.On("GetTaskType").Return(&tasktype) - return &judgeable - }, - problems.Status{CompilerOutput: "\na", Compiled: false}, - nil, - []judge.Response{}, - }, - } - - for _, test := range tests { - t.Run(test.Name, func(t *testing.T) { - sbp := sandbox.NewProvider() - s1, _ := sandbox.NewDummy() - sbp.Put(s1) - s2, _ := sandbox.NewDummy() - sbp.Put(s2) - - ch := make(chan judge.Response) - cb := judge.NewChanCallback(ch) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - ind := 0 - for resp := range ch { - if !cmp.Equal(test.Responses[ind], resp) { - t.Errorf("%v != %v", test.Responses[ind], resp) - } - ind++ - } - - if ind != len(test.Responses) { - t.Error("wrong number of responses") - } - - wg.Done() - }() - - w := judge.NewWorker(1, sbp) - ret, err := w.Judge(context.Background(), zap.NewNop(), test.Judgeable(), []byte(""), nil, cb) - close(ch) - - if !cmp.Equal(ret, test.JudgeReturnStatus) { - t.Errorf("%v != %v", ret, test.JudgeReturnStatus) - } - - if err != test.JudgeReturnErr { - t.Errorf("%s != %s", err, test.JudgeReturnErr) - } - - wg.Wait() - }) - } -} diff --git a/internal/judge2/server.go b/internal/judge2/server.go deleted file mode 100644 index c57ef107..00000000 --- a/internal/judge2/server.go +++ /dev/null @@ -1,194 +0,0 @@ -package judge2 - -import ( - "encoding/json" - "errors" - "fmt" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - "github.com/mraron/njudge/pkg/language" - "github.com/mraron/njudge/pkg/problems" - _ "github.com/mraron/njudge/pkg/problems/config/feladat_txt" - _ "github.com/mraron/njudge/pkg/problems/config/polygon" - _ "github.com/mraron/njudge/pkg/problems/config/problem_yaml" - _ "github.com/mraron/njudge/pkg/problems/config/task_yaml" - slogecho "github.com/samber/slog-echo" - "log/slog" - "net/http" - "time" - - _ "github.com/mraron/njudge/pkg/language/langs/python3" -) - -type Submission struct { - ID string `json:"id"` - Problem string `json:"problem"` - Language string `json:"language"` - Source []byte `json:"source"` - //TODO add status skeleton here? -} - -type Result struct { - Index int `json:"index,omitempty"` - Test string `json:"test,omitempty"` - Status *problems.Status `json:"status"` - Error string `json:"error,omitempty"` -} - -type Server struct { - Logger *slog.Logger - Judger Judger - ProblemStore problems.Store - - Config struct { - Port string - } -} - -type ServerOption func(*Server) - -func WithPortServerOption(port int) ServerOption { - return func(server *Server) { - server.Config.Port = fmt.Sprintf("%d", port) - } -} - -func NewServer(logger *slog.Logger, judger Judger, problemStore problems.Store, opts ...ServerOption) Server { - res := Server{Logger: logger, Judger: judger, ProblemStore: problemStore} - res.Config.Port = "8080" - for _, opt := range opts { - opt(&res) - } - return res -} - -func (s Server) PostJudgeHandler() echo.HandlerFunc { - return func(c echo.Context) error { - sub := Submission{} - if err := c.Bind(&sub); err != nil { - return err - } - - inited := false - initResponse := func(statusCode int) { - if inited { - return - } - inited = true - c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - c.Response().WriteHeader(statusCode) - } - enc := json.NewEncoder(c.Response().Writer) - - st, err := s.Judger.Judge(c.Request().Context(), sub, func(result Result) error { - initResponse(http.StatusOK) - return enc.Encode(result) - }) - res := Result{ - Status: st, - } - if err != nil { - res.Error = err.Error() - if errors.Is(err, problems.ErrorProblemNotFound) || errors.Is(err, language.ErrorLanguageNotFound) { - initResponse(http.StatusBadRequest) - return enc.Encode(res) - } - if st == nil { - return err - } - } - initResponse(http.StatusOK) - return enc.Encode(res) - } -} - -func (s Server) Run() error { - go func() { - for { - if err := s.ProblemStore.UpdateProblems(); err != nil { - s.Logger.Error("failed to update problemStore", err) - } - time.Sleep(30 * time.Second) - } - }() - - e := echo.New() - e.Use(slogecho.New(s.Logger)) - e.Use(middleware.Recover()) - - e.POST("/judge", s.PostJudgeHandler()) - - return e.Start(":" + s.Config.Port) -} - -/* -func main() { - s1, _ := sandbox.NewIsolate(104) - s2, _ := sandbox.NewIsolate(105) - provider := sandbox.NewProvider().Put(s1).Put(s2) - - problemStore := problems.NewFsStore("/home/aron/Projects/njudge/njudge_problems_git") - _ = problemStore.UpdateProblems() - languageStore := language.DefaultStore - - judge := Judge{ - SandboxProvider: provider, - ProblemStore: problemStore, - LanguageStore: languageStore, - RateLimit: 5 * time.Second, - } - - server := NewServer(slog.Default(), &judge, problemStore, WithPortServerOption(8081)) - go func() { - fmt.Println("start sleep") - time.Sleep(5 * time.Second) - ctx, cancel := context.WithCancel(context.Background()) - go func() { - time.Sleep(10 * time.Second) - cancel() - }() - client := NewClient("http://localhost:8081") - res, err := client.Judge(ctx, Submission{ - Problem: "KK24_csoki2", - Language: "python3", - Source: []byte(`while True: pass`), - }, func(result Result) error { - for _, tc := range result.Status.Feedback[0].Testcases() { - fmt.Print(tc.VerdictName) - } - fmt.Println("") - return nil - }) - fmt.Println("ends: ", res, err) - }() - panic(server.Run()) - /* - fmt.Println(judge.Judge(context.Background(), Submission{ - ID: "", - Problem: "KK24_csoki22", - Language: "python3", - Source: []byte(`// @check-accepted: examples N=0 no-limits - - #include - #include - #include - - using namespace std; - - int main() { - int M, N, K; - - cin >> M >> N >> K; - - if ( (N+M)%K == 0 ) { - cout << "IGEN" << endl; - } else { - cout << "NEM" << endl; - } - - return 0; - } - `), - }, nil)) -} -*/