From 296aa3f8a145a8fbc08db9f5b1d45fe6f72a38a4 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Mon, 5 Aug 2024 03:57:21 +0900 Subject: feat: implement task queue --- backend/taskqueue/processor.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 backend/taskqueue/processor.go (limited to 'backend/taskqueue/processor.go') diff --git a/backend/taskqueue/processor.go b/backend/taskqueue/processor.go new file mode 100644 index 0000000..e26ac64 --- /dev/null +++ b/backend/taskqueue/processor.go @@ -0,0 +1,25 @@ +package taskqueue + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/hibiken/asynq" +) + +type ExecProcessor struct { +} + +func (processor *ExecProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error { + var payload TaskExecPlayload + if err := json.Unmarshal(t.Payload(), &payload); err != nil { + return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry) + } + // TODO + return nil +} + +func NewExecProcessor() *ExecProcessor { + return &ExecProcessor{} +} -- cgit v1.2.3-70-g09d2 From 2fc239b3f4d49f1a257523df7c7781a2141252bf Mon Sep 17 00:00:00 2001 From: nsfisis Date: Mon, 5 Aug 2024 05:09:41 +0900 Subject: feat(backend): implement task queue processor --- backend/db/query.sql.go | 79 +++++++++++++++++ backend/main.go | 2 +- backend/query.sql | 13 +++ backend/taskqueue/processor.go | 176 ++++++++++++++++++++++++++++++++++++- backend/taskqueue/worker_server.go | 9 +- 5 files changed, 271 insertions(+), 8 deletions(-) (limited to 'backend/taskqueue/processor.go') diff --git a/backend/db/query.sql.go b/backend/db/query.sql.go index 8df3bf5..89506d0 100644 --- a/backend/db/query.sql.go +++ b/backend/db/query.sql.go @@ -11,6 +11,55 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const createSubmission = `-- name: CreateSubmission :one +INSERT INTO submissions (game_id, user_id, code, code_size) +VALUES ($1, $2, $3, $4) +RETURNING submission_id +` + +type CreateSubmissionParams struct { + GameID int32 + UserID int32 + Code string + CodeSize int32 +} + +func (q *Queries) CreateSubmission(ctx context.Context, arg CreateSubmissionParams) (int32, error) { + row := q.db.QueryRow(ctx, createSubmission, + arg.GameID, + arg.UserID, + arg.Code, + arg.CodeSize, + ) + var submission_id int32 + err := row.Scan(&submission_id) + return submission_id, err +} + +const createTestcaseExecution = `-- name: CreateTestcaseExecution :exec +INSERT INTO testcase_executions (submission_id, testcase_id, status, stdout, stderr) +VALUES ($1, $2, $3, $4, $5) +` + +type CreateTestcaseExecutionParams struct { + SubmissionID int32 + TestcaseID *int32 + Status string + Stdout string + Stderr string +} + +func (q *Queries) CreateTestcaseExecution(ctx context.Context, arg CreateTestcaseExecutionParams) error { + _, err := q.db.Exec(ctx, createTestcaseExecution, + arg.SubmissionID, + arg.TestcaseID, + arg.Status, + arg.Stdout, + arg.Stderr, + ) + return err +} + const getGameByID = `-- name: GetGameByID :one SELECT game_id, game_type, state, display_name, duration_seconds, created_at, started_at, games.problem_id, problems.problem_id, title, description FROM games LEFT JOIN problems ON games.problem_id = problems.problem_id @@ -263,6 +312,36 @@ func (q *Queries) ListGamesForPlayer(ctx context.Context, userID int32) ([]ListG return items, nil } +const listTestcasesByGameID = `-- name: ListTestcasesByGameID :many +SELECT testcase_id, problem_id, stdin, stdout FROM testcases +WHERE testcases.problem_id = (SELECT problem_id FROM games WHERE game_id = $1) +` + +func (q *Queries) ListTestcasesByGameID(ctx context.Context, gameID int32) ([]Testcase, error) { + rows, err := q.db.Query(ctx, listTestcasesByGameID, gameID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Testcase + for rows.Next() { + var i Testcase + if err := rows.Scan( + &i.TestcaseID, + &i.ProblemID, + &i.Stdin, + &i.Stdout, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listUsers = `-- name: ListUsers :many SELECT user_id, username, display_name, icon_path, is_admin, created_at FROM users ` diff --git a/backend/main.go b/backend/main.go index a213c6c..7cfbe2f 100644 --- a/backend/main.go +++ b/backend/main.go @@ -61,7 +61,7 @@ func main() { e.Use(middleware.Recover()) taskQueue := taskqueue.NewQueue("task-db:6379") - workerServer := taskqueue.NewWorkerServer("task-db:6379") + workerServer := taskqueue.NewWorkerServer("task-db:6379", queries) gameHubs := game.NewGameHubs(queries, taskQueue) err = gameHubs.RestoreFromDB(ctx) diff --git a/backend/query.sql b/backend/query.sql index 245d5cf..6395b9b 100644 --- a/backend/query.sql +++ b/backend/query.sql @@ -53,3 +53,16 @@ SET started_at = $6, problem_id = $7 WHERE game_id = $1; + +-- name: CreateSubmission :one +INSERT INTO submissions (game_id, user_id, code, code_size) +VALUES ($1, $2, $3, $4) +RETURNING submission_id; + +-- name: ListTestcasesByGameID :many +SELECT * FROM testcases +WHERE testcases.problem_id = (SELECT problem_id FROM games WHERE game_id = $1); + +-- name: CreateTestcaseExecution :exec +INSERT INTO testcase_executions (submission_id, testcase_id, status, stdout, stderr) +VALUES ($1, $2, $3, $4, $5); diff --git a/backend/taskqueue/processor.go b/backend/taskqueue/processor.go index e26ac64..1105da5 100644 --- a/backend/taskqueue/processor.go +++ b/backend/taskqueue/processor.go @@ -1,25 +1,193 @@ package taskqueue import ( + "bytes" "context" "encoding/json" "fmt" + "net/http" + "strings" "github.com/hibiken/asynq" + + "github.com/nsfisis/iosdc-japan-2024-albatross/backend/db" ) type ExecProcessor struct { + q *db.Queries +} + +func NewExecProcessor(q *db.Queries) *ExecProcessor { + return &ExecProcessor{ + q: q, + } } -func (processor *ExecProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error { +func (p *ExecProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error { var payload TaskExecPlayload if err := json.Unmarshal(t.Payload(), &payload); err != nil { return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry) } - // TODO + + // TODO: upsert + // Create submission record. + submissionID, err := p.q.CreateSubmission(ctx, db.CreateSubmissionParams{ + GameID: int32(payload.GameID), + UserID: int32(payload.UserID), + Code: payload.Code, + CodeSize: int32(len(payload.Code)), // TODO: exclude whitespaces. + }) + if err != nil { + return fmt.Errorf("CreateSubmission failed: %v", err) + } + + { + type swiftcRequestData struct { + MaxDuration int `json:"max_duration_ms"` + Code string `json:"code"` + } + type swiftcResponseData struct { + Result string `json:"result"` + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` + } + reqData := swiftcRequestData{ + MaxDuration: 5000, + Code: payload.Code, + } + reqJson, err := json.Marshal(reqData) + if err != nil { + return fmt.Errorf("json.Marshal failed: %v", err) + } + res, err := http.Post("http://worker:80/api/swiftc", "application/json", bytes.NewBuffer(reqJson)) + if err != nil { + return fmt.Errorf("http.Post failed: %v", err) + } + resData := swiftcResponseData{} + if err := json.NewDecoder(res.Body).Decode(&resData); err != nil { + return fmt.Errorf("json.Decode failed: %v", err) + } + if resData.Result != "success" { + err := p.q.CreateTestcaseExecution(ctx, db.CreateTestcaseExecutionParams{ + SubmissionID: submissionID, + TestcaseID: nil, + Status: "compile_error", + Stdout: resData.Stdout, + Stderr: resData.Stderr, + }) + if err != nil { + return fmt.Errorf("CreateTestcaseExecution failed: %v", err) + } + return fmt.Errorf("swiftc failed: %v", resData.Stderr) + } + } + { + type wasmcRequestData struct { + MaxDuration int `json:"max_duration_ms"` + Code string `json:"code"` + } + type wasmcResponseData struct { + Result string `json:"result"` + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` + } + reqData := wasmcRequestData{ + MaxDuration: 5000, + Code: payload.Code, + } + reqJson, err := json.Marshal(reqData) + if err != nil { + return fmt.Errorf("json.Marshal failed: %v", err) + } + res, err := http.Post("http://worker:80/api/wasmc", "application/json", bytes.NewBuffer(reqJson)) + if err != nil { + return fmt.Errorf("http.Post failed: %v", err) + } + resData := wasmcResponseData{} + if err := json.NewDecoder(res.Body).Decode(&resData); err != nil { + return fmt.Errorf("json.Decode failed: %v", err) + } + if resData.Result != "success" { + err := p.q.CreateTestcaseExecution(ctx, db.CreateTestcaseExecutionParams{ + SubmissionID: submissionID, + TestcaseID: nil, + Status: "compile_error", + Stdout: resData.Stdout, + Stderr: resData.Stderr, + }) + if err != nil { + return fmt.Errorf("CreateTestcaseExecution failed: %v", err) + } + return fmt.Errorf("wasmc failed: %v", resData.Stderr) + } + } + + testcases, err := p.q.ListTestcasesByGameID(ctx, int32(payload.GameID)) + if err != nil { + return fmt.Errorf("ListTestcasesByGameID failed: %v", err) + } + + for _, testcase := range testcases { + type testrunRequestData struct { + MaxDuration int `json:"max_duration_ms"` + Code string `json:"code"` + Stdin string `json:"stdin"` + } + type testrunResponseData struct { + Result string `json:"result"` + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` + } + reqData := testrunRequestData{ + MaxDuration: 5000, + Code: payload.Code, + Stdin: testcase.Stdin, + } + reqJson, err := json.Marshal(reqData) + if err != nil { + return fmt.Errorf("json.Marshal failed: %v", err) + } + res, err := http.Post("http://worker:80/api/testrun", "application/json", bytes.NewBuffer(reqJson)) + if err != nil { + return fmt.Errorf("http.Post failed: %v", err) + } + resData := testrunResponseData{} + if err := json.NewDecoder(res.Body).Decode(&resData); err != nil { + return fmt.Errorf("json.Decode failed: %v", err) + } + if resData.Result != "success" { + err := p.q.CreateTestcaseExecution(ctx, db.CreateTestcaseExecutionParams{ + SubmissionID: submissionID, + TestcaseID: &testcase.TestcaseID, + Status: resData.Result, + Stdout: resData.Stdout, + Stderr: resData.Stderr, + }) + if err != nil { + return fmt.Errorf("CreateTestcaseExecution failed: %v", err) + } + return fmt.Errorf("testrun failed: %v", resData.Stderr) + } + if isTestcaseExecutionCorrect(testcase.Stdout, resData.Stdout) { + err := p.q.CreateTestcaseExecution(ctx, db.CreateTestcaseExecutionParams{ + SubmissionID: submissionID, + TestcaseID: &testcase.TestcaseID, + Status: "wrong_answer", + Stdout: resData.Stdout, + Stderr: resData.Stderr, + }) + if err != nil { + return fmt.Errorf("CreateTestcaseExecution failed: %v", err) + } + return fmt.Errorf("testrun failed: %v", resData.Stdout) + } + } + return nil } -func NewExecProcessor() *ExecProcessor { - return &ExecProcessor{} +func isTestcaseExecutionCorrect(expectedStdout, actualStdout string) bool { + expectedStdout = strings.TrimSpace(expectedStdout) + actualStdout = strings.TrimSpace(actualStdout) + return actualStdout == expectedStdout } diff --git a/backend/taskqueue/worker_server.go b/backend/taskqueue/worker_server.go index 9bdd81f..09d9761 100644 --- a/backend/taskqueue/worker_server.go +++ b/backend/taskqueue/worker_server.go @@ -2,13 +2,16 @@ package taskqueue import ( "github.com/hibiken/asynq" + + "github.com/nsfisis/iosdc-japan-2024-albatross/backend/db" ) type WorkerServer struct { - server *asynq.Server + server *asynq.Server + queries *db.Queries } -func NewWorkerServer(redisAddr string) *WorkerServer { +func NewWorkerServer(redisAddr string, queries *db.Queries) *WorkerServer { return &WorkerServer{ server: asynq.NewServer( asynq.RedisClientOpt{ @@ -21,7 +24,7 @@ func NewWorkerServer(redisAddr string) *WorkerServer { func (s *WorkerServer) Run() error { mux := asynq.NewServeMux() - mux.Handle(TaskTypeExec, NewExecProcessor()) + mux.Handle(TaskTypeExec, NewExecProcessor(s.queries)) return s.server.Run(mux) } -- cgit v1.2.3-70-g09d2 From dc16e903999af89d87364ad6619e7c8b41301da4 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Mon, 5 Aug 2024 05:35:37 +0900 Subject: feat: show execution result in play page --- backend/api/generated.go | 58 ++++++++++++---------- backend/game/hub.go | 16 ++++++ backend/main.go | 2 +- backend/taskqueue/processor.go | 8 ++- backend/taskqueue/worker_server.go | 7 ++- frontend/app/.server/api/schema.d.ts | 2 +- frontend/app/components/GolfPlayApp.client.tsx | 6 ++- .../components/GolfPlayApps/GolfPlayAppGaming.tsx | 5 +- openapi.yaml | 5 ++ 9 files changed, 76 insertions(+), 33 deletions(-) (limited to 'backend/taskqueue/processor.go') diff --git a/backend/api/generated.go b/backend/api/generated.go index e371c7d..3c52f84 100644 --- a/backend/api/generated.go +++ b/backend/api/generated.go @@ -41,7 +41,12 @@ const ( // Defines values for GamePlayerMessageS2CExecResultPayloadStatus. const ( - GamePlayerMessageS2CExecResultPayloadStatusSuccess GamePlayerMessageS2CExecResultPayloadStatus = "success" + GamePlayerMessageS2CExecResultPayloadStatusCompileError GamePlayerMessageS2CExecResultPayloadStatus = "compile_error" + GamePlayerMessageS2CExecResultPayloadStatusFailure GamePlayerMessageS2CExecResultPayloadStatus = "failure" + GamePlayerMessageS2CExecResultPayloadStatusInternalError GamePlayerMessageS2CExecResultPayloadStatus = "internal_error" + GamePlayerMessageS2CExecResultPayloadStatusSuccess GamePlayerMessageS2CExecResultPayloadStatus = "success" + GamePlayerMessageS2CExecResultPayloadStatusTimeout GamePlayerMessageS2CExecResultPayloadStatus = "timeout" + GamePlayerMessageS2CExecResultPayloadStatusWrongAnswer GamePlayerMessageS2CExecResultPayloadStatus = "wrong_answer" ) // Defines values for GameWatcherMessageS2CExecResultPayloadStatus. @@ -1101,31 +1106,32 @@ func (sh *strictHandler) GetToken(ctx echo.Context, params GetTokenParams) error // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "5c8RuYXQw9qcZwBmWZb9GwAA//9z5sw+kyEAAA==", + "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", + "G+PQ1yEAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/backend/game/hub.go b/backend/game/hub.go index 0b12ce9..bb82170 100644 --- a/backend/game/hub.go +++ b/backend/game/hub.go @@ -34,6 +34,7 @@ type gameHub struct { watchers map[*watcherClient]bool registerWatcher chan *watcherClient unregisterWatcher chan *watcherClient + testcaseExecution chan string } func newGameHub(ctx context.Context, game *game, q *db.Queries, taskQueue *taskqueue.Queue) *gameHub { @@ -49,6 +50,7 @@ func newGameHub(ctx context.Context, game *game, q *db.Queries, taskQueue *taskq watchers: make(map[*watcherClient]bool), registerWatcher: make(chan *watcherClient), unregisterWatcher: make(chan *watcherClient), + testcaseExecution: make(chan string), } } @@ -185,6 +187,16 @@ func (hub *gameHub) run() { default: log.Printf("unexpected message type: %T", message.message) } + case executionStatus := <-hub.testcaseExecution: + for player := range hub.players { + player.s2cMessages <- &playerMessageS2CExecResult{ + Type: playerMessageTypeS2CExecResult, + Data: playerMessageS2CExecResultPayload{ + Score: nil, + Status: api.GamePlayerMessageS2CExecResultPayloadStatus(executionStatus), + }, + } + } case <-ticker.C: if hub.game.state == gameStateStarting { if time.Now().After(*hub.game.startedAt) { @@ -344,3 +356,7 @@ func (hubs *GameHubs) StartGame(gameID int) error { } return hub.startGame() } + +func (hubs *GameHubs) C() chan string { + return hubs.hubs[4].testcaseExecution +} diff --git a/backend/main.go b/backend/main.go index 7cfbe2f..e3d0052 100644 --- a/backend/main.go +++ b/backend/main.go @@ -61,7 +61,6 @@ func main() { e.Use(middleware.Recover()) taskQueue := taskqueue.NewQueue("task-db:6379") - workerServer := taskqueue.NewWorkerServer("task-db:6379", queries) gameHubs := game.NewGameHubs(queries, taskQueue) err = gameHubs.RestoreFromDB(ctx) @@ -98,6 +97,7 @@ func main() { gameHubs.Run() + workerServer := taskqueue.NewWorkerServer("task-db:6379", queries, gameHubs.C()) go func() { workerServer.Run() }() diff --git a/backend/taskqueue/processor.go b/backend/taskqueue/processor.go index 1105da5..e505c5a 100644 --- a/backend/taskqueue/processor.go +++ b/backend/taskqueue/processor.go @@ -15,11 +15,13 @@ import ( type ExecProcessor struct { q *db.Queries + c chan string } -func NewExecProcessor(q *db.Queries) *ExecProcessor { +func NewExecProcessor(q *db.Queries, c chan string) *ExecProcessor { return &ExecProcessor{ q: q, + c: c, } } @@ -78,6 +80,7 @@ func (p *ExecProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error { if err != nil { return fmt.Errorf("CreateTestcaseExecution failed: %v", err) } + p.c <- "compile_error" return fmt.Errorf("swiftc failed: %v", resData.Stderr) } } @@ -118,6 +121,7 @@ func (p *ExecProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error { if err != nil { return fmt.Errorf("CreateTestcaseExecution failed: %v", err) } + p.c <- "compile_error" return fmt.Errorf("wasmc failed: %v", resData.Stderr) } } @@ -166,6 +170,7 @@ func (p *ExecProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error { if err != nil { return fmt.Errorf("CreateTestcaseExecution failed: %v", err) } + p.c <- resData.Result return fmt.Errorf("testrun failed: %v", resData.Stderr) } if isTestcaseExecutionCorrect(testcase.Stdout, resData.Stdout) { @@ -179,6 +184,7 @@ func (p *ExecProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error { if err != nil { return fmt.Errorf("CreateTestcaseExecution failed: %v", err) } + p.c <- "wrong_answer" return fmt.Errorf("testrun failed: %v", resData.Stdout) } } diff --git a/backend/taskqueue/worker_server.go b/backend/taskqueue/worker_server.go index 09d9761..485d6d3 100644 --- a/backend/taskqueue/worker_server.go +++ b/backend/taskqueue/worker_server.go @@ -9,9 +9,10 @@ import ( type WorkerServer struct { server *asynq.Server queries *db.Queries + c chan string } -func NewWorkerServer(redisAddr string, queries *db.Queries) *WorkerServer { +func NewWorkerServer(redisAddr string, queries *db.Queries, c chan string) *WorkerServer { return &WorkerServer{ server: asynq.NewServer( asynq.RedisClientOpt{ @@ -19,12 +20,14 @@ func NewWorkerServer(redisAddr string, queries *db.Queries) *WorkerServer { }, asynq.Config{}, ), + queries: queries, + c: c, } } func (s *WorkerServer) Run() error { mux := asynq.NewServeMux() - mux.Handle(TaskTypeExec, NewExecProcessor(s.queries)) + mux.Handle(TaskTypeExec, NewExecProcessor(s.queries, s.c)) return s.server.Run(mux) } diff --git a/frontend/app/.server/api/schema.d.ts b/frontend/app/.server/api/schema.d.ts index 62badcf..6981dea 100644 --- a/frontend/app/.server/api/schema.d.ts +++ b/frontend/app/.server/api/schema.d.ts @@ -150,7 +150,7 @@ export interface components { * @example success * @enum {string} */ - status: "success"; + status: "success" | "failure" | "timeout" | "internal_error" | "compile_error" | "wrong_answer"; /** @example 100 */ score: number | null; }; diff --git a/frontend/app/components/GolfPlayApp.client.tsx b/frontend/app/components/GolfPlayApp.client.tsx index 80e7182..911fae0 100644 --- a/frontend/app/components/GolfPlayApp.client.tsx +++ b/frontend/app/components/GolfPlayApp.client.tsx @@ -73,6 +73,8 @@ export default function GolfPlayApp({ const [currentScore, setCurrentScore] = useState(null); + const [lastExecStatus, setLastExecStatus] = useState(null); + const onCodeChange = useDebouncedCallback((code: string) => { console.log("player:c2s:code"); sendJsonMessage({ @@ -121,13 +123,14 @@ export default function GolfPlayApp({ setGameState("starting"); } } else if (lastJsonMessage.type === "player:s2c:execresult") { - const { score } = lastJsonMessage.data; + const { status, score } = lastJsonMessage.data; if ( score !== null && (currentScore === null || score < currentScore) ) { setCurrentScore(score); } + setLastExecStatus(status); } } else { setGameState("waiting"); @@ -150,6 +153,7 @@ export default function GolfPlayApp({ onCodeChange={onCodeChange} onCodeSubmit={onCodeSubmit} currentScore={currentScore} + lastExecStatus={lastExecStatus} /> ); } else if (gameState === "finished") { diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx index 9fddb01..1a08b98 100644 --- a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx +++ b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx @@ -5,6 +5,7 @@ type Props = { onCodeChange: (code: string) => void; onCodeSubmit: (code: string) => void; currentScore: number | null; + lastExecStatus: string | null; }; export default function GolfPlayAppGaming({ @@ -12,6 +13,7 @@ export default function GolfPlayAppGaming({ onCodeChange, onCodeSubmit, currentScore, + lastExecStatus, }: Props) { const textareaRef = useRef(null); @@ -36,7 +38,8 @@ export default function GolfPlayAppGaming({
- Score: {currentScore == null ? "-" : `${currentScore}`} + Score: {currentScore == null ? "-" : `${currentScore}`} ( + {lastExecStatus})