Implements a package cache to avoid reparsing and extracting problem packages for subsequent submissions, improving performance and reducing resource consumption. Introduces an interface and a concurrent dictionary-based implementation for the cache. A processing lock is also implemented using a semaphore to avoid concurrent access to the same package.
LiquidCode Tester
Система автоматической проверки решений задач по программированию (Online Judge) для платформы LiquidCode.
Архитектура
Проект состоит из трёх основных компонентов:
1. Gateway (Шлюз)
- Технология: ASP.NET Core Web API
- Функции:
- Приём запросов на тестирование через REST API
- Скачивание пакетов задач из Polygon
- Маршрутизация запросов к соответствующим Worker'ам по языку программирования
2. Worker (Универсальный тестировщик)
- Технология: ASP.NET Core Web API + компиляторы всех поддерживаемых языков
- Функции:
- Динамический выбор компилятора/интерпретатора на основе языка
- Компиляция пользовательского кода (C++, Java, Kotlin, C#) или подготовка к исполнению (Python)
- Запуск в изолированной среде
- Тестирование на наборе тестов
- Контроль ограничений по времени и памяти
- Отправка статусов на callback URL
3. Common (Общая библиотека)
- Технология: .NET Class Library
- Функции: Общие модели данных для Gateway и Worker
Поддерживаемые языки
- C++ (реализовано)
- Java (реализовано)
- Kotlin (реализовано)
- C# (реализовано)
- Python (реализовано)
Модель данных
SubmitForTesterModel (запрос на тестирование)
public record SubmitForTesterModel(
long Id,
long MissionId,
string Language,
string LanguageVersion,
string SourceCode,
string PackageUrl,
string CallbackUrl
);
TesterResponseModel (результат тестирования)
public record TesterResponseModel(
long SubmitId,
State State, // Waiting, Compiling, Testing, Done
ErrorCode ErrorCode, // None, CompileError, RuntimeError, etc.
string Message,
int CurrentTest,
int AmountOfTests
);
Быстрый старт
Локальная разработка (Docker Compose)
# Сборка и запуск всех сервисов
docker-compose up --build
# Gateway доступен на http://localhost:8080
# Worker доступен на http://localhost:8081
Разработка без Docker
# Сборка всего решения
dotnet build
# Запуск Gateway
cd src/LiquidCode.Tester.Gateway
dotnet run
# Запуск Worker (в другом терминале)
cd src/LiquidCode.Tester.Worker
dotnet run
Локальное тестирование
Для тестирования системы без внешних зависимостей используйте endpoint /api/Tester/submit-local:
1. Подготовка тестового пакета
Создайте ZIP файл с тестами в формате:
test-package.zip
├── 1.in # Входные данные для теста 1
├── 1.out # Ожидаемый результат для теста 1
├── 2.in # Входные данные для теста 2
├── 2.out # Ожидаемый результат для теста 2
└── ...
2. Отправка запроса
curl -X 'POST' \
'http://localhost:8080/api/Tester/submit-local' \
-F 'id=1' \
-F 'missionId=1' \
-F 'language=python' \
-F 'languageVersion=3.11' \
-F 'sourceCode=n = int(input())
print(n * 2)' \
-F 'callbackUrl=log' \
-F 'package=@./test-package.zip'
3. Просмотр результатов
При использовании callbackUrl=log результаты тестирования выводятся в консоль Worker:
╔═══════════════════════════════════════════════════════════════╗
║ CALLBACK RESULT ║
╠═══════════════════════════════════════════════════════════════╣
{
"submitId": 1,
"state": "Done",
"errorCode": "None",
"message": "All tests passed",
"currentTest": 3,
"amountOfTests": 3
}
╚═══════════════════════════════════════════════════════════════╝
Специальные значения callbackUrl
"log"- Вывод результатов в логи Worker"console"- Аналогично "log""log://"- Аналогично "log"- Любой HTTP URL - Отправка результатов на указанный endpoint
API Endpoints
Gateway
POST /api/tester/submit
Отправка решения с удаленным пакетом тестов:
{
"id": 123,
"missionId": 456,
"language": "C++",
"languageVersion": "17",
"sourceCode": "#include <iostream>\nint main() { ... }",
"packageUrl": "https://example.com/package.zip",
"callbackUrl": "https://example.com/api/submit/callback"
}
POST /api/tester/submit-local
Отправка решения с локальным пакетом тестов (для отладки):
curl -X 'POST' \
'http://localhost:8080/api/Tester/submit-local' \
-F 'id=1' \
-F 'missionId=1' \
-F 'language=python' \
-F 'languageVersion=3.11' \
-F 'sourceCode=print("Hello, World!")' \
-F 'callbackUrl=log' \
-F 'package=@/path/to/test-package.zip'
Параметры:
id- ID сабмитаmissionId- ID задачиlanguage- Язык программирования (C++, Java, Kotlin, C#, Python)languageVersion- Версия языка (опционально, используется "latest" по умолчанию)sourceCode- Исходный код решенияcallbackUrl- URL для отправки результатов или"log"для вывода в консольpackage- ZIP файл с тестами
GET /api/tester/health
- Проверка состояния Gateway
Worker
POST /api/test
- Принимает multipart/form-data с метаданными и ZIP файлом пакета
GET /api/test/health
- Проверка состояния Worker
Процесс тестирования
- Приём запроса: Gateway получает запрос с кодом и URL пакета
- Скачивание пакета: Gateway скачивает Polygon пакет
- Маршрутизация: Gateway отправляет запрос в Worker для нужного языка
- Парсинг: Worker распаковывает и парсит пакет (тесты, лимиты)
- Компиляция: Worker выбирает нужный компилятор на основе языка и компилирует код
- Тестирование: Worker последовательно запускает все тесты
- Callback: Worker отправляет статусы на callback URL на каждом этапе
- Cleanup: Worker удаляет временные файлы
Изоляция и безопасность
Текущая реализация
- Процессы запускаются с ограничением по времени
- Контроль памяти через PeakWorkingSet64
- Процессы убиваются при превышении лимитов
Production рекомендации
- Linux namespaces (PID, network, mount)
- cgroups для контроля CPU и памяти
- seccomp для ограничения системных вызовов
- AppArmor/SELinux профили
- chroot окружение
- Отключение доступа к сети для тестируемого кода
Конфигурация
Gateway (appsettings.json)
{
"PackageDownloadDirectory": "/tmp/packages",
"Workers": {
"Cpp": "http://localhost:8081",
"Java": "http://localhost:8081",
"Kotlin": "http://localhost:8081",
"CSharp": "http://localhost:8081",
"Python": "http://localhost:8081"
}
}
Worker (appsettings.json)
{
"Cpp": {
"Compiler": "g++",
"CompilerFlags": "-O2 -std=c++17 -Wall",
"Versions": {
"14": {
"Compiler": "g++",
"CompilerFlags": "-O2 -std=c++14 -Wall"
},
"17": {
"Compiler": "g++",
"CompilerFlags": "-O2 -std=c++17 -Wall"
},
"20": {
"Compiler": "g++",
"CompilerFlags": "-O2 -std=c++20 -Wall"
}
}
},
"Java": {
"Compiler": "javac",
"CompilerFlags": "",
"Versions": {
"8": {
"Compiler": "javac",
"CompilerFlags": "-source 8 -target 8"
},
"11": {
"Compiler": "javac",
"CompilerFlags": "-source 11 -target 11"
},
"17": {
"Compiler": "javac",
"CompilerFlags": ""
}
}
},
"Kotlin": {
"Compiler": "kotlinc",
"CompilerFlags": "",
"Versions": {
"1.9": {
"Compiler": "kotlinc",
"CompilerFlags": ""
}
}
},
"CSharp": {
"Compiler": "csc",
"CompilerFlags": "/optimize+",
"Versions": {
"7": {
"Compiler": "csc",
"CompilerFlags": "/optimize+ /langversion:7"
},
"8": {
"Compiler": "csc",
"CompilerFlags": "/optimize+ /langversion:8"
},
"9": {
"Compiler": "csc",
"CompilerFlags": "/optimize+ /langversion:9"
}
}
},
"Python": {
"Executable": "python3",
"Versions": {
"3.8": {
"Executable": "python3.8"
},
"3.9": {
"Executable": "python3.9"
},
"3.10": {
"Executable": "python3.10"
},
"3.11": {
"Executable": "python3.11"
}
}
}
}
Поддержка версий языков
Система поддерживает указание версии языка программирования через параметр languageVersion:
- "latest" или null - использует версию по умолчанию из основной конфигурации
- Конкретная версия (например, "17", "3.11") - использует конфигурацию из секции
Versions - Несуществующая версия - логируется предупреждение и используется версия по умолчанию
Каждый язык имеет свою конфигурацию с возможностью указания:
- Для компилируемых языков (C++, Java, Kotlin, C#): путь к компилятору и флаги компиляции
- Для интерпретируемых языков (Python): путь к интерпретатору
Пример использования:
# Использование Python 3.11
curl -X POST 'http://localhost:8080/api/Tester/submit-local' \
-F 'languageVersion=3.11' \
-F 'language=python' \
...
# Использование C++20
curl -X POST 'http://localhost:8080/api/Tester/submit-local' \
-F 'languageVersion=20' \
-F 'language=C++' \
...
Структура проекта
LiquidCode.Tester/
├── src/
│ ├── LiquidCode.Tester.Common/ # Общие модели
│ │ └── Models/
│ ├── LiquidCode.Tester.Gateway/ # API Gateway
│ │ ├── Controllers/
│ │ ├── Services/
│ │ └── Dockerfile
│ └── LiquidCode.Tester.Worker/ # Универсальный Worker (все языки)
│ ├── Controllers/
│ ├── Services/
│ │ ├── Compilation/ # Компиляторы
│ │ ├── Execution/ # Исполнители
│ │ └── Factories/ # Фабрики
│ └── Dockerfile
├── compose.yaml # Docker Compose
└── README.md
Требования
- .NET 9.0 SDK
- Docker & Docker Compose (для контейнеризации)
- Компиляторы языков программирования (устанавливаются автоматически в Docker):
- g++ (C++)
- OpenJDK 17 (Java)
- Kotlin compiler (Kotlin)
- Mono (C#)
- Python 3
Расширение на другие языки
Для добавления поддержки нового языка:
- Создайте новую реализацию
ICompilationServiceдля языка (например,GoCompilationService.cs) - Создайте новую реализацию
IExecutionServiceдля языка (например,GoExecutionService.cs) - Зарегистрируйте новые сервисы в
Program.cs:builder.Services.AddSingleton<GoCompilationService>(); builder.Services.AddSingleton<GoExecutionService>(); - Обновите фабрики (
CompilationServiceFactoryиExecutionServiceFactory), добавив поддержку нового языка - Добавьте конфигурацию языка в
appsettings.jsonWorker - Обновите
DockerfileWorker, добавив установку компилятора/runtime - Добавьте конфигурацию воркера для нового языка в
appsettings.jsonGateway - Обновите
WorkerClientServiceGateway, добавив маппинг языка на URL воркера
Разработка
Структура кода
- Controllers: REST API endpoints
- Services: Бизнес-логика (компиляция, тестирование, callback)
- Models: Модели данных
Логирование
Используется встроенный ILogger ASP.NET Core.
Тестирование
dotnet test
Лицензия
[Укажите лицензию]
Авторы
LiquidCode Team