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

@@ -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"
}
}