This commit is contained in:
prixod
2025-10-24 23:29:56 +04:00
parent 8af94da831
commit 3d854c3470
18 changed files with 330 additions and 45 deletions

View File

@@ -12,12 +12,14 @@
</component>
<component name="ChangeListManager">
<list default="true" id="1d3190f0-8175-44b9-bab6-12e025e4819d" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/.dockerignore" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
<change afterPath="$PROJECT_DIR$/LiquidCode.Tester.sln" afterDir="false" />
<change afterPath="$PROJECT_DIR$/global.json" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/LiquidCode.Tester.Common/Dockerfile" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/LiquidCode.Tester.Worker/Dockerfile" afterDir="false" />
<change beforePath="$PROJECT_DIR$/global.json" beforeDir="false" afterPath="$PROJECT_DIR$/global.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/LiquidCode.Tester.Common/LiquidCode.Tester.Common.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/src/LiquidCode.Tester.Common/LiquidCode.Tester.Common.csproj" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/LiquidCode.Tester.Common/Program.cs" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/LiquidCode.Tester.Gateway/LiquidCode.Tester.Gateway.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/src/LiquidCode.Tester.Gateway/LiquidCode.Tester.Gateway.csproj" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/LiquidCode.Tester.Gateway/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/LiquidCode.Tester.Gateway/Program.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/LiquidCode.Tester.Gateway/appsettings.Development.json" beforeDir="false" afterPath="$PROJECT_DIR$/src/LiquidCode.Tester.Gateway/appsettings.Development.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/LiquidCode.Tester.Gateway/appsettings.json" beforeDir="false" afterPath="$PROJECT_DIR$/src/LiquidCode.Tester.Gateway/appsettings.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/LiquidCode.Tester.Worker/LiquidCode.Tester.Worker.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/src/LiquidCode.Tester.Worker/LiquidCode.Tester.Worker.csproj" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -263,8 +265,17 @@
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1761331001679</updated>
<workItem from="1761331002792" duration="1650000" />
<workItem from="1761331002792" duration="2278000" />
</task>
<task id="LOCAL-00001" summary="init">
<option name="closed" value="true" />
<created>1761333540672</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1761333540672</updated>
</task>
<option name="localTasksCounter" value="2" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -272,8 +283,21 @@
</component>
<component name="UnityCheckinConfiguration" checkUnsavedScenes="true" />
<component name="UnityProjectConfiguration" hasMinimizedUI="false" />
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
</component>
<component name="VcsManagerConfiguration">
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
<MESSAGE value="init" />
<option name="LAST_COMMIT_MESSAGE" value="init" />
</component>
<component name="XDebuggerManager">
<breakpoint-manager>

View File

@@ -1,7 +1,7 @@
{
"sdk": {
"version": "9.0.0",
"version": "10.0.100-rc.2.25502.107",
"rollForward": "latestMinor",
"allowPrerelease": false
"allowPrerelease": true
}
}

View File

@@ -1,11 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>

View File

@@ -0,0 +1,12 @@
namespace LiquidCode.Tester.Common.Models;
public enum ErrorCode
{
None,
CompileError,
RuntimeError,
MemoryError,
TimeLimitError,
IncorrectAnswer,
UnknownError
}

View File

@@ -0,0 +1,9 @@
namespace LiquidCode.Tester.Common.Models;
public enum State
{
Waiting,
Compiling,
Testing,
Done
}

View File

@@ -0,0 +1,11 @@
namespace LiquidCode.Tester.Common.Models;
public record SubmitForTesterModel(
long Id,
long MissionId,
string Language,
string LanguageVersion,
string SourceCode,
string PackageUrl,
string CallbackUrl
);

View File

@@ -0,0 +1,10 @@
namespace LiquidCode.Tester.Common.Models;
public record TesterResponseModel(
long SubmitId,
State State,
ErrorCode ErrorCode,
string Message,
int CurrentTest,
int AmountOfTests
);

View File

@@ -1,3 +0,0 @@
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

View File

@@ -0,0 +1,52 @@
using LiquidCode.Tester.Common.Models;
using LiquidCode.Tester.Gateway.Services;
using Microsoft.AspNetCore.Mvc;
namespace LiquidCode.Tester.Gateway.Controllers;
[ApiController]
[Route("api/[controller]")]
public class TesterController : ControllerBase
{
private readonly IPackageDownloadService _packageDownloadService;
private readonly IWorkerClientService _workerClientService;
private readonly ILogger<TesterController> _logger;
public TesterController(
IPackageDownloadService packageDownloadService,
IWorkerClientService workerClientService,
ILogger<TesterController> logger)
{
_packageDownloadService = packageDownloadService;
_workerClientService = workerClientService;
_logger = logger;
}
[HttpPost("submit")]
public async Task<IActionResult> Submit([FromBody] SubmitForTesterModel request)
{
_logger.LogInformation("Received submit request for ID {SubmitId}", request.Id);
try
{
// Download the package
var packagePath = await _packageDownloadService.DownloadPackageAsync(request.PackageUrl);
// Send to appropriate worker based on language
await _workerClientService.SendToWorkerAsync(request, packagePath);
return Accepted(new { message = "Submit accepted for testing", submitId = request.Id });
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process submit {SubmitId}", request.Id);
return StatusCode(500, new { error = "Failed to process submit", details = ex.Message });
}
}
[HttpGet("health")]
public IActionResult Health()
{
return Ok(new { status = "healthy", timestamp = DateTime.UtcNow });
}
}

View File

@@ -11,6 +11,10 @@
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LiquidCode.Tester.Common\LiquidCode.Tester.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>

View File

@@ -1,41 +1,26 @@
using LiquidCode.Tester.Gateway.Services;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
// Add services to the container
builder.Services.AddControllers();
builder.Services.AddOpenApi();
// Add HttpClient
builder.Services.AddHttpClient();
// Register application services
builder.Services.AddSingleton<IPackageDownloadService, PackageDownloadService>();
builder.Services.AddSingleton<IWorkerClientService, WorkerClientService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
app.MapControllers();
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

View File

@@ -0,0 +1,11 @@
namespace LiquidCode.Tester.Gateway.Services;
public interface IPackageDownloadService
{
/// <summary>
/// Downloads a package from the specified URL
/// </summary>
/// <param name="packageUrl">URL to download the package from</param>
/// <returns>Path to the downloaded package file</returns>
Task<string> DownloadPackageAsync(string packageUrl);
}

View File

@@ -0,0 +1,13 @@
using LiquidCode.Tester.Common.Models;
namespace LiquidCode.Tester.Gateway.Services;
public interface IWorkerClientService
{
/// <summary>
/// Sends a submit to the appropriate worker based on the language
/// </summary>
/// <param name="submit">Submit data</param>
/// <param name="packagePath">Local path to the downloaded package</param>
Task SendToWorkerAsync(SubmitForTesterModel submit, string packagePath);
}

View File

@@ -0,0 +1,49 @@
namespace LiquidCode.Tester.Gateway.Services;
public class PackageDownloadService : IPackageDownloadService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<PackageDownloadService> _logger;
private readonly string _downloadDirectory;
public PackageDownloadService(
IHttpClientFactory httpClientFactory,
ILogger<PackageDownloadService> logger,
IConfiguration configuration)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
_downloadDirectory = configuration["PackageDownloadDirectory"] ?? Path.Combine(Path.GetTempPath(), "packages");
if (!Directory.Exists(_downloadDirectory))
{
Directory.CreateDirectory(_downloadDirectory);
}
}
public async Task<string> DownloadPackageAsync(string packageUrl)
{
_logger.LogInformation("Downloading package from {Url}", packageUrl);
try
{
var httpClient = _httpClientFactory.CreateClient();
var response = await httpClient.GetAsync(packageUrl);
response.EnsureSuccessStatusCode();
var fileName = $"package_{Guid.NewGuid()}.zip";
var filePath = Path.Combine(_downloadDirectory, fileName);
await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
await response.Content.CopyToAsync(fileStream);
_logger.LogInformation("Package downloaded successfully to {Path}", filePath);
return filePath;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to download package from {Url}", packageUrl);
throw;
}
}
}

View File

@@ -0,0 +1,92 @@
using System.Net.Http.Headers;
using LiquidCode.Tester.Common.Models;
namespace LiquidCode.Tester.Gateway.Services;
public class WorkerClientService : IWorkerClientService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<WorkerClientService> _logger;
private readonly IConfiguration _configuration;
public WorkerClientService(
IHttpClientFactory httpClientFactory,
ILogger<WorkerClientService> logger,
IConfiguration configuration)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
_configuration = configuration;
}
public async Task SendToWorkerAsync(SubmitForTesterModel submit, string packagePath)
{
var workerUrl = GetWorkerUrlForLanguage(submit.Language);
_logger.LogInformation("Sending submit {SubmitId} to worker at {WorkerUrl}", submit.Id, workerUrl);
try
{
var httpClient = _httpClientFactory.CreateClient();
using var form = new MultipartFormDataContent();
// Add submit metadata
form.Add(new StringContent(submit.Id.ToString()), "Id");
form.Add(new StringContent(submit.MissionId.ToString()), "MissionId");
form.Add(new StringContent(submit.Language), "Language");
form.Add(new StringContent(submit.LanguageVersion), "LanguageVersion");
form.Add(new StringContent(submit.SourceCode), "SourceCode");
form.Add(new StringContent(submit.CallbackUrl), "CallbackUrl");
// Add package file
var fileStream = File.OpenRead(packagePath);
var fileContent = new StreamContent(fileStream);
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
form.Add(fileContent, "Package", Path.GetFileName(packagePath));
var response = await httpClient.PostAsync($"{workerUrl}/api/test", form);
response.EnsureSuccessStatusCode();
_logger.LogInformation("Submit {SubmitId} sent successfully to worker", submit.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send submit {SubmitId} to worker", submit.Id);
throw;
}
finally
{
// Clean up downloaded package
try
{
if (File.Exists(packagePath))
{
File.Delete(packagePath);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to delete package file {Path}", packagePath);
}
}
}
private string GetWorkerUrlForLanguage(string language)
{
var workerUrl = language.ToLowerInvariant() switch
{
"c++" => _configuration["Workers:Cpp"],
"java" => _configuration["Workers:Java"],
"kotlin" => _configuration["Workers:Kotlin"],
"c#" => _configuration["Workers:CSharp"],
_ => throw new NotSupportedException($"Language {language} is not supported")
};
if (string.IsNullOrEmpty(workerUrl))
{
throw new InvalidOperationException($"Worker URL for language {language} is not configured");
}
return workerUrl;
}
}

View File

@@ -4,5 +4,9 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"PackageDownloadDirectory": "C:\\temp\\packages",
"Workers": {
"Cpp": "http://localhost:8081"
}
}

View File

@@ -5,5 +5,12 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"PackageDownloadDirectory": "/tmp/packages",
"Workers": {
"Cpp": "http://liquidcode-tester-worker-cpp:8080",
"Java": "http://liquidcode-tester-worker-java:8080",
"Kotlin": "http://liquidcode-tester-worker-kotlin:8080",
"CSharp": "http://liquidcode-tester-worker-csharp:8080"
}
}

View File

@@ -1,13 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LiquidCode.Tester.Common\LiquidCode.Tester.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="..\..\.dockerignore">
<Link>.dockerignore</Link>