From fed73f184eeca55448a524004936608cc08806f0 Mon Sep 17 00:00:00 2001 From: Vyacheslav1557 Date: Sat, 12 Apr 2025 20:05:04 +0500 Subject: [PATCH 1/4] refactor: rename submodule refactor: rename submodule refactor: rename submodule --- .gitmodules | 2 +- Makefile | 2 +- config.yaml | 2 +- contracts | 1 + internal/tester/delivery.go | 2 +- internal/tester/delivery/rest/handlers.go | 2 +- main.go | 2 +- proto | 1 - 8 files changed, 7 insertions(+), 7 deletions(-) create mode 160000 contracts delete mode 160000 proto diff --git a/.gitmodules b/.gitmodules index b358abe..6fd0375 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "proto"] - path = proto + path = contracts url = https://git.sch9.ru/new_gate/contracts diff --git a/Makefile b/Makefile index eb340fb..5a607f0 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ tag = latest gen: - @oapi-codegen --config=config.yaml ./proto/tester/v1/openapi.yaml + @oapi-codegen --config=config.yaml ./contracts/tester/v1/openapi.yaml dev: gen @go run main.go build: gen diff --git a/config.yaml b/config.yaml index 7e2a9cf..10eaf26 100644 --- a/config.yaml +++ b/config.yaml @@ -2,4 +2,4 @@ package: testerv1 generate: fiber-server: true models: true -output: ./proto/tester/v1/tester.go \ No newline at end of file +output: ./contracts/tester/v1/tester.go \ No newline at end of file diff --git a/contracts b/contracts new file mode 160000 index 0000000..f00483d --- /dev/null +++ b/contracts @@ -0,0 +1 @@ +Subproject commit f00483d24a53a243734c793fc24e02d52d39fdab diff --git a/internal/tester/delivery.go b/internal/tester/delivery.go index d1045a9..c978917 100644 --- a/internal/tester/delivery.go +++ b/internal/tester/delivery.go @@ -1,7 +1,7 @@ package tester import ( - testerv1 "git.sch9.ru/new_gate/ms-tester/proto/tester/v1" + testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1" "github.com/gofiber/fiber/v2" ) diff --git a/internal/tester/delivery/rest/handlers.go b/internal/tester/delivery/rest/handlers.go index cd5bbb3..fc7fdf5 100644 --- a/internal/tester/delivery/rest/handlers.go +++ b/internal/tester/delivery/rest/handlers.go @@ -1,10 +1,10 @@ package rest import ( + testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1" "git.sch9.ru/new_gate/ms-tester/internal/models" "git.sch9.ru/new_gate/ms-tester/internal/tester" "git.sch9.ru/new_gate/ms-tester/pkg" - testerv1 "git.sch9.ru/new_gate/ms-tester/proto/tester/v1" "github.com/gofiber/fiber/v2" "io" ) diff --git a/main.go b/main.go index cfb5686..ce4db72 100644 --- a/main.go +++ b/main.go @@ -3,11 +3,11 @@ package main import ( "fmt" "git.sch9.ru/new_gate/ms-tester/config" + testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1" "git.sch9.ru/new_gate/ms-tester/internal/tester/delivery/rest" problemsRepository "git.sch9.ru/new_gate/ms-tester/internal/tester/repository" testerUseCase "git.sch9.ru/new_gate/ms-tester/internal/tester/usecase" "git.sch9.ru/new_gate/ms-tester/pkg" - testerv1 "git.sch9.ru/new_gate/ms-tester/proto/tester/v1" "github.com/gofiber/fiber/v2" fiberlogger "github.com/gofiber/fiber/v2/middleware/logger" "github.com/ilyakaznacheev/cleanenv" diff --git a/proto b/proto deleted file mode 160000 index 1fbee7b..0000000 --- a/proto +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1fbee7ba29c358c76d1c835ac6999ce9e1b59ee9 From a28d2506e57fdf593b1ef22041756dabdab24c15 Mon Sep 17 00:00:00 2001 From: Vyacheslav1557 Date: Sat, 12 Apr 2025 23:49:40 +0500 Subject: [PATCH 2/4] docs: add readme --- README.md | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..2b71266 --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# ms-tester + +`ms-tester` is a microservice designed for managing programming competitions. It provides backend functionality for handling problems, contests, participants, and their submissions. The service is developed in Go. PostgreSQL serves as the relational database. Pandoc is used to convert problem statements from LaTeX to HTML. + +For understanding the architecture, see the [documentation](https://git.sch9.ru/new_gate/docs). + +### Prerequisites + +Before you begin, ensure you have the following dependencies installed: + +* **Docker** and **Docker Compose**: To run PostgreSQL, Pandoc. +* **Goose**: For applying database migrations (`go install github.com/pressly/goose/v3/cmd/goose@latest`). +* **oapi-codegen**: For generating OpenAPI code (`go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest`). + +### 1. Running Dependencies + +You can run PostgreSQL and Pandoc using Docker Compose, for example: + +```yaml +version: '3.8' +services: + pandoc: + image: pandoc/latex + ports: + - "4000:3030" # Exposes Pandoc server on port 4000 locally + command: "server" # Runs Pandoc in server mode + postgres: + image: postgres:14.1-alpine # Uses PostgreSQL 14.1 Alpine image + restart: always # Ensures the container restarts if it stops + environment: + POSTGRES_USER: postgres # Default user + POSTGRES_PASSWORD: supersecretpassword # Default password (change for production!) + POSTGRES_DB: postgres # Default database name + ports: + - '5432:5432' # Exposes PostgreSQL on the standard port 5432 + volumes: + - ./postgres-data:/var/lib/postgresql/data # Persists database data locally + healthcheck: + test: pg_isready -U postgres -d postgres # Command to check if PostgreSQL is ready + interval: 10s # Check every 10 seconds + timeout: 3s # Wait 3 seconds for the check to respond + retries: 5 # Try 5 times before marking as unhealthy +volumes: + postgres-data: # Defines the named volume for data persistence +``` + +Start the services in detached mode: + +```bash +docker-compose up -d +``` + +### 2. Configuration + +The application uses environment variables for configuration. Create a `.env` file in the project root. The minimum required variables are: + +```dotenv +# Environment type (development or production) +ENV=dev # or prod + +# Address of the running Pandoc service +PANDOC=http://localhost:4000 + +# Address and port where the ms-tester service will listen +ADDRESS=localhost:8080 + +# PostgreSQL connection string (Data Source Name) +# Format: postgres://user:password@host:port/database?sslmode=disable +POSTGRES_DSN=postgres://username:supersecretpassword@localhost:5432/db_name?sslmode=disable + +# Secret key for signing and verifying JWT tokens +JWT_SECRET=your_super_secret_jwt_key +``` + +**Important:** Replace `supersecretpassword` and `your_super_secret_jwt_key` with secure, unique values, especially for a production environment. + +### 3. Database Migrations + +The project uses `goose` to manage the database schema. + +1. Ensure `goose` is installed: + ```bash + go install github.com/pressly/goose/v3/cmd/goose@latest + ``` +2. Apply the migrations to the running PostgreSQL database. Make sure the connection string in the command matches the `POSTGRES_DSN` from your `.env` file: + ```bash + goose -dir ./migrations postgres "postgres://postgres:supersecretpassword@localhost:5432/postgres?sslmode=disable" up + ``` + +### 4. OpenAPI Code Generation + +The project uses OpenAPI to define its API. Go code for handlers and models is generated based on this specification using `oapi-codegen`. + +Run the generation command: + +```bash +make gen +``` + +### 5. Running the Application + +Start the `ms-tester` service: + +```bash +go run ./main.go +``` + +After starting, the service will be available at the address specified in the `ADDRESS` variable in your `.env` file (e.g., `http://localhost:8080`). \ No newline at end of file From 94cb1f5e66345c051a6f998aff26fd6489e535ae Mon Sep 17 00:00:00 2001 From: maxminds Date: Sun, 13 Apr 2025 16:55:34 +0500 Subject: [PATCH 3/4] Added max amount of tasks on contest --- migrations/20240727123308_initial.sql | 42 ++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/migrations/20240727123308_initial.sql b/migrations/20240727123308_initial.sql index 25632a6..87a50ad 100644 --- a/migrations/20240727123308_initial.sql +++ b/migrations/20240727123308_initial.sql @@ -9,6 +9,24 @@ BEGIN END; $$; +CREATE FUNCTION check_max_tasks() RETURNS TRIGGER + LANGUAGE plpgsql AS +$$ +DECLARE + max_on_contest_tasks_amount integer := 50; +BEGIN + IF ( + SELECT count(*) FROM tasks + WHERE contest_id = NEW.contest_id + ) >= ( + max_on_contest_tasks_amount + ) THEN + RAISE EXCEPTION 'Exceeded max tasks for this contest'; +END IF; + RETURN NEW; +END; +$$; + CREATE TABLE IF NOT EXISTS problems ( id serial NOT NULL, @@ -73,6 +91,11 @@ CREATE TABLE IF NOT EXISTS tasks CHECK (position >= 0) ); +CREATE TRIGGER max_tasks_on_contest_check + BEFORE INSERT ON tasks + FOR EACH STATEMENT +EXECUTE FUNCTION check_max_tasks(); + CREATE TRIGGER on_tasks_update BEFORE UPDATE ON tasks @@ -98,6 +121,7 @@ CREATE TRIGGER on_participants_update FOR EACH ROW EXECUTE PROCEDURE updated_at_update(); + CREATE TABLE IF NOT EXISTS solutions ( id serial NOT NULL, @@ -125,15 +149,17 @@ EXECUTE PROCEDURE updated_at_update(); -- +goose Down -- +goose StatementBegin +DROP TRIGGER IF EXISTS on_solutions_update ON solutions; +DROP TABLE IF EXISTS solutions; +DROP TRIGGER IF EXISTS on_participants_update ON participants; +DROP TABLE IF EXISTS participants; +DROP TRIGGER IF EXISTS on_tasks_update ON tasks; +DROP TRIGGER IF EXISTS max_tasks_on_contest_check ON tasks; +DROP TABLE IF EXISTS tasks; DROP TRIGGER IF EXISTS on_problems_update ON problems; DROP TABLE IF EXISTS problems; DROP TRIGGER IF EXISTS on_contests_update ON contests; DROP TABLE IF EXISTS contests; -DROP TRIGGER IF EXISTS on_tasks_update ON tasks; -DROP TABLE IF EXISTS tasks; -DROP TRIGGER IF EXISTS on_participants_update ON participants; -DROP TABLE IF EXISTS participants; -DROP TRIGGER IF EXISTS on_solutions_update ON solutions; -DROP TABLE IF EXISTS solutions; -DROP FUNCTION updated_at_update(); --- +goose StatementEnd +DROP FUNCTION IF EXISTS updated_at_update(); +DROP FUNCTION IF EXISTS check_max_tasks(); +-- +goose StatementEnd \ No newline at end of file From adcc1bc8d887d4bc9e28644ff32de6a66e1ff4d4 Mon Sep 17 00:00:00 2001 From: OXYgen Date: Sun, 13 Apr 2025 17:01:57 +0500 Subject: [PATCH 4/4] read the best solution for each task in contest --- internal/tester/delivery/rest/handlers.go | 15 ++++- internal/tester/pg_repository.go | 1 + .../repository/pg_contests_repository.go | 62 +++++++++++++++++++ internal/tester/usecase.go | 1 + internal/tester/usecase/contests_usecase.go | 4 ++ 5 files changed, 82 insertions(+), 1 deletion(-) diff --git a/internal/tester/delivery/rest/handlers.go b/internal/tester/delivery/rest/handlers.go index cd5bbb3..7ec9cfd 100644 --- a/internal/tester/delivery/rest/handlers.go +++ b/internal/tester/delivery/rest/handlers.go @@ -99,6 +99,15 @@ func (h *TesterHandlers) GetContest(c *fiber.Ctx, id int32) error { return c.SendStatus(pkg.ToREST(err)) } + var participantId int32 = 2 + + solutions, err := h.contestsUC.ReadBestSolutions(c.Context(), id, participantId) + + m := make(map[int32]*models.Solution) + + for i := 0; i < len(solutions); i++ { + m[solutions[i].TaskPosition] = solutions[i] + } resp := testerv1.GetContestResponse{ Contest: C2C(*contest), Tasks: make([]struct { @@ -108,11 +117,15 @@ func (h *TesterHandlers) GetContest(c *fiber.Ctx, id int32) error { } for i, task := range tasks { + solution := testerv1.Solution{} + if sol, ok := m[task.Position]; ok { + solution = S2S(*sol) + } resp.Tasks[i] = struct { Solution testerv1.Solution `json:"solution"` Task testerv1.TasksListItem `json:"task"` }{ - Solution: testerv1.Solution{}, + Solution: solution, Task: TLI2TLI(*task), } } diff --git a/internal/tester/pg_repository.go b/internal/tester/pg_repository.go index 81883ca..78b29cf 100644 --- a/internal/tester/pg_repository.go +++ b/internal/tester/pg_repository.go @@ -49,4 +49,5 @@ type ContestRepository interface { ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) ReadTask(ctx context.Context, id int32) (*models.Task, error) ReadMonitor(ctx context.Context, id int32) (*models.Monitor, error) + ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error) } diff --git a/internal/tester/repository/pg_contests_repository.go b/internal/tester/repository/pg_contests_repository.go index ec73a08..94c34e2 100644 --- a/internal/tester/repository/pg_contests_repository.go +++ b/internal/tester/repository/pg_contests_repository.go @@ -626,3 +626,65 @@ func (r *ContestRepository) ReadMonitor(ctx context.Context, contestId int32) (* return &monitor, nil } + +const ( + // state=5 - AC + readBestSolutions = ` + WITH contest_tasks AS ( + SELECT t.id AS task_id, + t.position AS task_position, + t.contest_id, + t.problem_id, + t.created_at, + t.updated_at, + p.title AS task_title, + c.title AS contest_title + FROM tasks t + LEFT JOIN problems p ON p.id = t.problem_id + LEFT JOIN contests c ON c.id = t.contest_id + WHERE t.contest_id = ? +), + best_solutions AS ( + SELECT DISTINCT ON (s.task_id) + * + FROM solutions s + WHERE s.participant_id = ? + ORDER BY s.task_id, s.score DESC, s.created_at DESC + ) +SELECT + s.id, + s.participant_id, + p.name AS participant_name, + s.solution, + s.state, + s.score, + s.penalty, + s.time_stat, + s.memory_stat, + s.language, + ct.task_id, + ct.task_position, + ct.task_title, + ct.contest_id, + ct.contest_title, + s.updated_at, + s.created_at +FROM contest_tasks ct + LEFT JOIN best_solutions s ON s.task_id = ct.task_id + LEFT JOIN participants p ON p.id = s.participant_id WHERE s.id IS NOT NULL +ORDER BY ct.task_position +` +) + +func (r *ContestRepository) ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error) { + const op = "ContestRepository.ReadBestSolutions" + var solutions []*models.Solution + query := r.db.Rebind(readBestSolutions) + err := r.db.SelectContext(ctx, &solutions, query, contestId, participantId) + + if err != nil { + return nil, handlePgErr(err, op) + } + + return solutions, nil +} diff --git a/internal/tester/usecase.go b/internal/tester/usecase.go index 9ddf235..a473dca 100644 --- a/internal/tester/usecase.go +++ b/internal/tester/usecase.go @@ -31,4 +31,5 @@ type ContestUseCase interface { ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) ReadTask(ctx context.Context, id int32) (*models.Task, error) ReadMonitor(ctx context.Context, id int32) (*models.Monitor, error) + ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error) } diff --git a/internal/tester/usecase/contests_usecase.go b/internal/tester/usecase/contests_usecase.go index 414c9a2..28761dd 100644 --- a/internal/tester/usecase/contests_usecase.go +++ b/internal/tester/usecase/contests_usecase.go @@ -85,3 +85,7 @@ func (uc *ContestUseCase) ReadTask(ctx context.Context, id int32) (*models.Task, func (uc *ContestUseCase) ReadMonitor(ctx context.Context, contestId int32) (*models.Monitor, error) { return uc.contestRepo.ReadMonitor(ctx, contestId) } + +func (uc *ContestUseCase) ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error) { + return uc.contestRepo.ReadBestSolutions(ctx, contestId, participantId) +}