From 7b4cb6a5d3d55968444551addea0bf85508a80e2 Mon Sep 17 00:00:00 2001 From: Seif Mohamed Date: Mon, 25 May 2026 21:56:19 +0300 Subject: [PATCH] Refactor ExecutionService and remove memory tracking --- .../Execution/IExecutionService.cs | 11 +- .../RunCode/RunCodeCommandHandler.cs | 3 +- .../SubmitSolutionCommandHandler.cs | 3 +- src/CodeClash.Domain/Premitives/Helper.cs | 14 +- .../Premitives/Responses/AcceptedResponse.cs | 13 + .../Responses/BaseSubmissionResponse.cs | 8 +- .../Responses/RunTimeErrorResponse.cs | 5 + .../Responses/TimeLimitExceedResponse.cs | 2 + .../Responses/WrongAnswerResponse.cs | 1 + .../Implementation/ExecutionService.cs | 275 ++++++++---------- 10 files changed, 155 insertions(+), 180 deletions(-) diff --git a/src/CodeClash.Application/Abstractions/Execution/IExecutionService.cs b/src/CodeClash.Application/Abstractions/Execution/IExecutionService.cs index 819543b..9e3f819 100644 --- a/src/CodeClash.Application/Abstractions/Execution/IExecutionService.cs +++ b/src/CodeClash.Application/Abstractions/Execution/IExecutionService.cs @@ -1,5 +1,4 @@ using CodeClash.Application.DTO; -using CodeClash.Domain.Models.TestCases; using CodeClash.Domain.Premitives; using CodeClash.Domain.Premitives.Responses; @@ -17,13 +16,5 @@ Task RunCodeAsync( string code, Language language, List testCases, - decimal runTimeLimit, - decimal memoryLimit); - - Task RunCodeAsync( - string code, - Language language, - IEnumerable testCases, - decimal runTimeLimit, - decimal memoryLimit); + decimal runTimeLimit); } diff --git a/src/CodeClash.Application/RunCode/RunCodeCommandHandler.cs b/src/CodeClash.Application/RunCode/RunCodeCommandHandler.cs index 5afa833..f7fe775 100644 --- a/src/CodeClash.Application/RunCode/RunCodeCommandHandler.cs +++ b/src/CodeClash.Application/RunCode/RunCodeCommandHandler.cs @@ -41,8 +41,7 @@ public async Task> Handle( codeContent, request.Language, testCasesDtos, - problem.RunTimeLimit, - (int)problem.MemoryLimit); + problem.RunTimeLimit); var response = new RunCodeResponse( Input: request.CustomTestcasesJson, diff --git a/src/CodeClash.Application/SolveProblem/SubmitSolutions/SubmitSolutionCommandHandler.cs b/src/CodeClash.Application/SolveProblem/SubmitSolutions/SubmitSolutionCommandHandler.cs index 8c6b943..cdb8f91 100644 --- a/src/CodeClash.Application/SolveProblem/SubmitSolutions/SubmitSolutionCommandHandler.cs +++ b/src/CodeClash.Application/SolveProblem/SubmitSolutions/SubmitSolutionCommandHandler.cs @@ -65,8 +65,7 @@ public async Task> Handle( codeContent, request.Language, testCasesDtos, - problem.RunTimeLimit, - problem.MemoryLimit); + problem.RunTimeLimit); var submission = await request.ToEntityAsync(userId); diff --git a/src/CodeClash.Domain/Premitives/Helper.cs b/src/CodeClash.Domain/Premitives/Helper.cs index 3a87031..21f0af8 100644 --- a/src/CodeClash.Domain/Premitives/Helper.cs +++ b/src/CodeClash.Domain/Premitives/Helper.cs @@ -8,13 +8,6 @@ public static class Helper public const string CppCompiler = "gcc:latest"; public const string CSharpCompiler = "mcr.microsoft.com/dotnet/sdk:5.0"; - //public static string ExecuteCodeCommand(string containerId, decimal runTime) - //{ - // string runTimeLimit = $"{runTime}s"; - // // Prepare the docker exec command to run the script inside the container - // return $"docker exec {containerId} /usr/bin/bash /run_code.sh {runTimeLimit}"; - //} - public static string SetScriptFilePath() { string currentDirectory = Directory.GetCurrentDirectory(); @@ -48,14 +41,13 @@ private static bool DirectoryContainsFile( public static string CreateExecuteCodeCommand( string containerId, - decimal timeLimit, - decimal memoryLimit) + decimal timeLimit) { string runTimeLimit = $"{timeLimit}s"; - string runMemoryLimit = $"{Math.Round(memoryLimit)}mb"; + // string runMemoryLimit = $"{Math.Round(memoryLimit)}mb"; - return $"docker exec {containerId} /usr/bin/bash /run_code.sh {runTimeLimit} {runMemoryLimit}"; + return $"docker exec {containerId} /usr/bin/bash /run_code.sh {runTimeLimit}"; } public static decimal ExtractExecutionTime(string time) diff --git a/src/CodeClash.Domain/Premitives/Responses/AcceptedResponse.cs b/src/CodeClash.Domain/Premitives/Responses/AcceptedResponse.cs index 118239e..842c2d7 100644 --- a/src/CodeClash.Domain/Premitives/Responses/AcceptedResponse.cs +++ b/src/CodeClash.Domain/Premitives/Responses/AcceptedResponse.cs @@ -1,5 +1,18 @@ namespace CodeClash.Domain.Premitives.Responses; public sealed class AcceptedResponse : BaseSubmissionResponse { + public decimal ExecutionTime { get; set; } + public AcceptedResponse() + { + + } + + public AcceptedResponse(BaseSubmissionResponse baseResponse) + { + SubmissionDate = baseResponse.SubmissionDate; + SubmissionResult = baseResponse.SubmissionResult; + TotalTestcases = baseResponse.TotalTestcases; + NumberOfPassedTestCases = baseResponse.NumberOfPassedTestCases; + } } diff --git a/src/CodeClash.Domain/Premitives/Responses/BaseSubmissionResponse.cs b/src/CodeClash.Domain/Premitives/Responses/BaseSubmissionResponse.cs index 492ab20..9bd0f41 100644 --- a/src/CodeClash.Domain/Premitives/Responses/BaseSubmissionResponse.cs +++ b/src/CodeClash.Domain/Premitives/Responses/BaseSubmissionResponse.cs @@ -1,9 +1,7 @@ namespace CodeClash.Domain.Premitives.Responses; public class BaseSubmissionResponse { - public decimal ExecutionTime { get; set; } - - public decimal ExecutionMemory { get; set; } + //public decimal ExecutionMemory { get; set; } // public string Code { get; set; } @@ -11,7 +9,7 @@ public class BaseSubmissionResponse public SubmissionResult SubmissionResult { get; set; } = SubmissionResult.Accepted; - public int NumberOfPassedTestCases { get; set; } + public int TotalTestcases { get; set; } - public string Input { get; set; } = string.Empty; + public int NumberOfPassedTestCases { get; set; } } diff --git a/src/CodeClash.Domain/Premitives/Responses/RunTimeErrorResponse.cs b/src/CodeClash.Domain/Premitives/Responses/RunTimeErrorResponse.cs index 08e5cf1..48bd222 100644 --- a/src/CodeClash.Domain/Premitives/Responses/RunTimeErrorResponse.cs +++ b/src/CodeClash.Domain/Premitives/Responses/RunTimeErrorResponse.cs @@ -2,4 +2,9 @@ public sealed class RunTimeErrorResponse : BaseSubmissionResponse { public string Message { get; set; } + + public string Input { get; set; } + + public string ExpectedOutput { get; set; } + } diff --git a/src/CodeClash.Domain/Premitives/Responses/TimeLimitExceedResponse.cs b/src/CodeClash.Domain/Premitives/Responses/TimeLimitExceedResponse.cs index 5a33c55..e1bccce 100644 --- a/src/CodeClash.Domain/Premitives/Responses/TimeLimitExceedResponse.cs +++ b/src/CodeClash.Domain/Premitives/Responses/TimeLimitExceedResponse.cs @@ -1,5 +1,7 @@ namespace CodeClash.Domain.Premitives.Responses; public sealed class TimeLimitExceedResponse : BaseSubmissionResponse { + public string Input { get; set; } + public string ExpectedOutput { get; set; } } diff --git a/src/CodeClash.Domain/Premitives/Responses/WrongAnswerResponse.cs b/src/CodeClash.Domain/Premitives/Responses/WrongAnswerResponse.cs index 4e508a5..d6e2b85 100644 --- a/src/CodeClash.Domain/Premitives/Responses/WrongAnswerResponse.cs +++ b/src/CodeClash.Domain/Premitives/Responses/WrongAnswerResponse.cs @@ -1,6 +1,7 @@ namespace CodeClash.Domain.Premitives.Responses; public sealed class WrongAnswerResponse : BaseSubmissionResponse { + public string Input { get; set; } public string ExpectedOutput { get; set; } public string ActualOutput { get; set; } } diff --git a/src/CodeClash.Infrastructure/Implementation/ExecutionService.cs b/src/CodeClash.Infrastructure/Implementation/ExecutionService.cs index a905193..d817156 100644 --- a/src/CodeClash.Infrastructure/Implementation/ExecutionService.cs +++ b/src/CodeClash.Infrastructure/Implementation/ExecutionService.cs @@ -13,7 +13,7 @@ namespace CodeClash.Infrastructure.Implementation; /// Service responsible for executing user code inside Docker containers /// and evaluating results against test cases. /// -internal sealed class ExecutionService : IExecutionService +internal sealed class ExecutionService : IExecutionService, IDisposable { // Docker client to communicate with Docker engine private readonly DockerClient _dockerClient; @@ -25,14 +25,13 @@ internal sealed class ExecutionService : IExecutionService private readonly string _requestDirectory; // Docker container ID used for execution - private string _containerId; + private string? _containerId; // Paths for execution artifacts - private readonly string outputFile; - private readonly string errorFile; - private readonly string runTimeFile; - private readonly string runTimeErrorFile; - // private readonly string memoryFile; + private readonly string _outputFile; + private readonly string _errorFile; + private readonly string _runTimeFile; + private readonly string _runTimeErrorFile; // Command to keep container alive (idle) internal static readonly string[] parameters = new[] { "tail", "-f", "/dev/null" }; @@ -49,154 +48,93 @@ public ExecutionService(IFileService fileService) Directory.CreateDirectory(_requestDirectory); // Define files used for communication with container - outputFile = Path.Combine(_requestDirectory, "output.txt"); - errorFile = Path.Combine(_requestDirectory, "error.txt"); - runTimeFile = Path.Combine(_requestDirectory, "runtime.txt"); - runTimeErrorFile = Path.Combine(_requestDirectory, "runtime_errors.txt"); - //memoryFile = Path.Combine(_requestDirectory, "memory.txt"); - // this.unitOfWork = unitOfWork; + _outputFile = Path.Combine(_requestDirectory, "output.txt"); + _errorFile = Path.Combine(_requestDirectory, "error.txt"); + _runTimeFile = Path.Combine(_requestDirectory, "runtime.txt"); + _runTimeErrorFile = Path.Combine(_requestDirectory, "runtime_errors.txt"); _fileService = fileService; } /// - /// Main method: executes code against multiple test cases. + /// Overload that accepts DTOs — maps to domain model and delegates. /// public async Task RunCodeAsync( string code, Language language, List testCases, - decimal runTimeLimit, - decimal memoryLimit) + decimal runTimeLimit) { - decimal maxRunTime = 0; + var domainTestCases = testCases + .Select(t => new Testcase { Input = t.Input, Output = t.Output }) + .ToList(); + + return await RunCodeAsync(code, language, domainTestCases, runTimeLimit); + } + + /// + /// Main method: executes code against multiple test cases. + /// + public async Task RunCodeAsync( + string code, + Language language, + List testCases, + decimal runTimeLimit) + { + decimal maxRunTime = 0m; try { await _fileService.CreateCodeFile(code, language, _requestDirectory); - - // create container await CreateAndStartContainer(language); - //await Task.Delay(10000); - for (int i = 0; i < testCases.Count; i++) { - await _fileService.CreateTestCasesFile(testCases[i].Input, _requestDirectory); - await ExecuteCodeInContainer(runTimeLimit, memoryLimit); + var testcase = testCases[i]; + int testcaseNumber = i + 1; - // Map DTO -> Domain model before passing - var testCase = new Testcase - { - Input = testCases[i].Input, - Output = testCases[i].Output, - }; + await _fileService.CreateTestCasesFile(testcase.Input, _requestDirectory); + await ExecuteCodeInContainer(runTimeLimit); - var result = await CalculateResult(testCase, runTimeLimit, i + 1, testCases[i].Input); + var result = await CalculateResult(testcase, testcaseNumber, testCases.Count); if (result.SubmissionResult != SubmissionResult.Accepted) { return result; } - maxRunTime = Math.Max(maxRunTime, result.ExecutionTime); + if (result is AcceptedResponse accepted) + { + maxRunTime = Math.Max(maxRunTime, accepted.ExecutionTime); + } } } - catch (Exception ex) { - throw new Exception("Error while running testcases !!!", ex); + throw new Exception("Error while running testcases.", ex); } - finally { - if (Directory.Exists(_requestDirectory)) - { - Directory.Delete(_requestDirectory, true); - } - - if (_containerId != null) - { - await _dockerClient.Containers.RemoveContainerAsync(_containerId, new ContainerRemoveParameters { Force = true }); - } + await CleanupAsync(); } - return new AcceptedResponse { ExecutionTime = maxRunTime, + NumberOfPassedTestCases = testCases.Count, + TotalTestcases = testCases.Count }; } - public async Task RunCodeAsync( - string code, - Language language, - IEnumerable testCases, // Changed to support multiple test cases - decimal runTimeLimit, - decimal memoryLimit) - { - // Create user code file - await _fileService.CreateCodeFile(code, language, _requestDirectory); - - decimal maxRunTime = 0m; - decimal maxMemory = 0m; - - var testcaseList = testCases.ToList(); - - for (int i = 0; i < testcaseList.Count; i++) - { - var testcase = testcaseList[i]; - int testcaseNumber = i + 1; - - // Create input file for this test case - await _fileService.CreateTestCasesFile(testcase.Input, _requestDirectory); - - // Start container fresh per test case - await CreateAndStartContainer(language); - - // Execute code - await ExecuteCodeInContainer(runTimeLimit, memoryLimit); - - // Evaluate result — return early on any failure - var result = await CalculateResult(testcase, runTimeLimit, testcaseNumber, testcase.Input); - - if (result.SubmissionResult != SubmissionResult.Accepted) - { - return result; - } - maxRunTime = Math.Max(maxRunTime, result.ExecutionTime); - maxMemory = Math.Max(maxMemory, result.ExecutionMemory); - } - - // All test cases passed - return new AcceptedResponse - { - ExecutionMemory = maxMemory, - NumberOfPassedTestCases = testcaseList.Count, - // ExecutionTime: ideally sum or max across runs — placeholder here - ExecutionTime = maxRunTime - }; - } - - /// - /// Reads execution outputs and determines result (AC, WA, TLE, etc.) + /// Reads execution outputs and determines result (AC, WA, TLE, CE, RTE). /// private async Task CalculateResult( Testcase testCase, - decimal runTimeLimit, int testcaseNumber, - string input) + int totalTestcases) { - string output = await _fileService.ReadFileAsync(outputFile); - string error = await _fileService.ReadFileAsync(errorFile); - string runTime = await _fileService.ReadFileAsync(runTimeFile); - string runTimeError = await _fileService.ReadFileAsync(runTimeErrorFile); - // string memory = await _fileService.ReadFileAsync(memoryFile); - - // Initialize the run result - // BaseSubmissionResponse response = default; - + var (output, error, runTime, runTimeError) = await ReadExecutionOutputsAsync(); if (!string.IsNullOrEmpty(error)) { @@ -204,29 +142,33 @@ private async Task CalculateResult( { Message = error, SubmissionResult = SubmissionResult.CompilationError, - ExecutionTime = 0m - + NumberOfPassedTestCases = 0, + TotalTestcases = totalTestcases }; } + if (!string.IsNullOrEmpty(runTimeError)) { return new RunTimeErrorResponse { Message = runTimeError, SubmissionResult = SubmissionResult.RunTimeError, + Input = testCase.Input, + TotalTestcases = totalTestcases, NumberOfPassedTestCases = testcaseNumber - 1, - ExecutionTime = 0, - Input = input + ExpectedOutput = testCase.Output }; } + if (runTime?.Contains("TIMELIMITEXCEEDED") == true) { return new TimeLimitExceedResponse { + Input = testCase.Input, NumberOfPassedTestCases = testcaseNumber - 1, - ExecutionTime = runTimeLimit, + TotalTestcases = totalTestcases, SubmissionResult = SubmissionResult.TimeLimitExceeded, - Input = input + ExpectedOutput = testCase.Output }; } @@ -235,31 +177,31 @@ private async Task CalculateResult( return new WrongAnswerResponse { NumberOfPassedTestCases = testcaseNumber - 1, + TotalTestcases = totalTestcases, ActualOutput = output, + Input = testCase.Input, ExpectedOutput = testCase.Output, SubmissionResult = SubmissionResult.WrongAnswer, - ExecutionTime = Helper.ExtractExecutionTime(runTime!), - Input = input }; } - return new AcceptedResponse { + NumberOfPassedTestCases = testcaseNumber, ExecutionTime = Helper.ExtractExecutionTime(runTime!), - //ExecutionMemory = Helper.ExtractExecutionMemory(memory) - ExecutionMemory = 3m, + TotalTestcases = totalTestcases }; } + /// + /// Overload for custom test cases (no expected output comparison). + /// private async Task CalculateResult( CustomTestcaseDto testcaseDto, - decimal runTimeLimit) + int testcaseNumber, + int totalTestcases) { - string output = await _fileService.ReadFileAsync(outputFile); - string error = await _fileService.ReadFileAsync(errorFile); - string runTime = await _fileService.ReadFileAsync(runTimeFile); - string runTimeError = await _fileService.ReadFileAsync(runTimeErrorFile); + var (output, error, runTime, runTimeError) = await ReadExecutionOutputsAsync(); if (!string.IsNullOrEmpty(error)) { @@ -267,7 +209,8 @@ private async Task CalculateResult( { Message = error, SubmissionResult = SubmissionResult.CompilationError, - ExecutionTime = 0m + NumberOfPassedTestCases = 0, + TotalTestcases = totalTestcases }; } @@ -277,7 +220,9 @@ private async Task CalculateResult( { Message = runTimeError, SubmissionResult = SubmissionResult.RunTimeError, - ExecutionTime = Helper.ExtractExecutionTime(runTime ?? string.Empty) + TotalTestcases = totalTestcases, + NumberOfPassedTestCases = testcaseNumber - 1, + ExpectedOutput = testcaseDto.ExpectedOutput }; } @@ -285,8 +230,11 @@ private async Task CalculateResult( { return new TimeLimitExceedResponse { - ExecutionTime = runTimeLimit, + Input = testcaseDto.Input, + NumberOfPassedTestCases = testcaseNumber - 1, + TotalTestcases = totalTestcases, SubmissionResult = SubmissionResult.TimeLimitExceeded, + ExpectedOutput = testcaseDto.ExpectedOutput }; } @@ -294,27 +242,41 @@ private async Task CalculateResult( { return new WrongAnswerResponse { + NumberOfPassedTestCases = testcaseNumber - 1, + TotalTestcases = totalTestcases, ActualOutput = output, + Input = testcaseDto.Input, ExpectedOutput = testcaseDto.ExpectedOutput, SubmissionResult = SubmissionResult.WrongAnswer, - ExecutionTime = Helper.ExtractExecutionTime(runTime ?? string.Empty) }; } return new AcceptedResponse { + NumberOfPassedTestCases = testcaseNumber, ExecutionTime = Helper.ExtractExecutionTime(runTime ?? string.Empty), - ExecutionMemory = 3m, + TotalTestcases = totalTestcases }; } /// - /// Creates and starts Docker container with correct compiler image + /// Reads all execution output files in one place — shared by both CalculateResult overloads. /// - private async Task CreateAndStartContainer( - Language language) + private async Task<(string output, string error, string runTime, string runTimeError)> ReadExecutionOutputsAsync() + { + return ( + await _fileService.ReadFileAsync(_outputFile), + await _fileService.ReadFileAsync(_errorFile), + await _fileService.ReadFileAsync(_runTimeFile), + await _fileService.ReadFileAsync(_runTimeErrorFile) + ); + } + + /// + /// Creates and starts Docker container with the correct compiler image. + /// + private async Task CreateAndStartContainer(Language language) { - // Select image based on language var image = language switch { Language.py => Helper.PythonCompiler, @@ -323,46 +285,38 @@ private async Task CreateAndStartContainer( _ => throw new ArgumentException("Unsupported language") }; - // Create container var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { HostConfig = new HostConfig { - // Mount temp directory and execution script Binds = new[] { $"{_requestDirectory}:/code", $"{Helper.ScriptFilePath}:/run_code.sh" }, - NetworkMode = "none", // disable internet access (security) - Memory = 256 * 1024 * 1024, // limit memory + NetworkMode = "bridge", // isolated bridge network (allows loopback, blocks external) + Memory = 256 * 1024 * 1024, // limit memory to 256 MB AutoRemove = false }, Name = "code_container", Image = image, - Cmd = parameters, // keep container alive + Cmd = parameters, // keep container alive }); _containerId = createContainerResponse.ID; - // Start container await _dockerClient.Containers.StartContainerAsync( _containerId, new ContainerStartParameters()); } /// - /// Executes code inside container using shell command + /// Executes code inside the container using a shell command. /// - private async Task ExecuteCodeInContainer( - decimal timeLimit, - decimal memoryLimit) + private async Task ExecuteCodeInContainer(decimal timeLimit) { - string command = Helper.CreateExecuteCodeCommand( - _containerId, - timeLimit, - memoryLimit); + string command = Helper.CreateExecuteCodeCommand(_containerId!, timeLimit); using var process = new System.Diagnostics.Process(); @@ -379,13 +333,34 @@ private async Task ExecuteCodeInContainer( try { process.Start(); - - // Wait until execution finishes await process.WaitForExitAsync(); } - catch (Exception) + catch (Exception ex) { - throw new Exception("Error While Executing Client Code !!"); + throw new Exception("Error while executing client code.", ex); } } + + /// + /// Cleans up temp directory and removes the Docker container. + /// + private async Task CleanupAsync() + { + if (Directory.Exists(_requestDirectory)) + { + Directory.Delete(_requestDirectory, true); + } + + if (!string.IsNullOrEmpty(_containerId)) + { + await _dockerClient.Containers.RemoveContainerAsync( + _containerId, + new ContainerRemoveParameters { Force = true }); + } + } + + /// + /// Disposes the Docker client. + /// + public void Dispose() => _dockerClient.Dispose(); }