Adds package caching to the testing service
All checks were successful
Build and Push Docker Images / build (src/LiquidCode.Tester.Gateway/Dockerfile, git.nullptr.top/liquidcode/liquidcode-tester-gateway-roman, gateway) (push) Successful in 1m31s
Build and Push Docker Images / build (src/LiquidCode.Tester.Worker/Dockerfile, git.nullptr.top/liquidcode/liquidcode-tester-worker-roman, worker) (push) Successful in 4m44s

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.
This commit is contained in:
2025-11-04 21:51:43 +03:00
parent bf7bd0ad6b
commit 8e6c2c222e
5 changed files with 309 additions and 39 deletions

View File

@@ -15,6 +15,7 @@ public class TestingServiceTests : IDisposable
private readonly Mock<IOutputCheckerService> _outputCheckerMock;
private readonly Mock<ICallbackService> _callbackServiceMock;
private readonly Mock<ILogger<TestingService>> _loggerMock;
private readonly IPackageCacheService _packageCache;
private readonly TestingService _service;
private readonly string _testDirectory;
@@ -26,9 +27,11 @@ public class TestingServiceTests : IDisposable
_outputCheckerMock = new Mock<IOutputCheckerService>();
_callbackServiceMock = new Mock<ICallbackService>();
_loggerMock = new Mock<ILogger<TestingService>>();
_packageCache = new PackageCacheService();
_service = new TestingService(
_packageParserMock.Object,
_packageCache,
_compilationFactoryMock.Object,
_executionFactoryMock.Object,
_outputCheckerMock.Object,
@@ -104,7 +107,7 @@ public class TestingServiceTests : IDisposable
await File.WriteAllTextAsync(inputFile, "test input");
await File.WriteAllTextAsync(outputFile, "expected output");
await File.WriteAllTextAsync(executablePath, "dummy");
await CreateEmptyPackage(packageFilePath);
await CreateEmptyPackage(packageFilePath);
var request = new TestRequest
{
@@ -194,11 +197,108 @@ public class TestingServiceTests : IDisposable
);
}
private async Task CreateEmptyPackage(string filePath)
[Fact]
public async Task ProcessSubmitAsync_ReusesCachedPackage()
{
// Arrange
var packageFilePath = Path.Combine(_testDirectory, "cached_package.zip");
var inputFile = Path.Combine(_testDirectory, "1.in");
var outputFile = Path.Combine(_testDirectory, "1.out");
var executablePath = Path.Combine(_testDirectory, "solution.exe");
await File.WriteAllTextAsync(inputFile, "test input");
await File.WriteAllTextAsync(outputFile, "expected output");
await File.WriteAllTextAsync(executablePath, "dummy");
await CreateEmptyPackage(packageFilePath);
var request = new TestRequest
{
Id = 123,
MissionId = 456,
Language = "cpp",
LanguageVersion = "17",
SourceCode = "int main() { return 0; }",
PackageFilePath = packageFilePath,
CallbackUrl = "http://localhost/callback"
};
var package = new ProblemPackage
{
WorkingDirectory = _testDirectory,
ExtractionRoot = _testDirectory,
TestCases = new List<TestCase>
{
new TestCase
{
Number = 1,
InputFilePath = inputFile,
OutputFilePath = outputFile,
TimeLimit = 2000,
MemoryLimit = 256
}
}
};
var compilationService = new Mock<ICompilationService>();
var executionService = new Mock<IExecutionService>();
var parseCalls = 0;
_packageParserMock
.Setup(x => x.ParsePackageAsync(It.IsAny<Stream>()))
.Callback(() => parseCalls++)
.ReturnsAsync(package);
_compilationFactoryMock
.Setup(x => x.GetCompilationService("cpp"))
.Returns(compilationService.Object);
_executionFactoryMock
.Setup(x => x.GetExecutionService("cpp"))
.Returns(executionService.Object);
compilationService
.Setup(x => x.CompileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(new CompilationResult
{
Success = true,
ExecutablePath = executablePath
});
executionService
.Setup(x => x.ExecuteAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.ReturnsAsync(new ExecutionResult
{
Success = true,
Output = "expected output",
ExitCode = 0,
RuntimeError = false,
TimeLimitExceeded = false,
MemoryLimitExceeded = false
});
_outputCheckerMock
.Setup(x => x.CheckOutputWithCheckerAsync(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string?>()))
.ReturnsAsync(true);
// Act
await _service.ProcessSubmitAsync(request);
await _service.ProcessSubmitAsync(request);
// Assert
Assert.Equal(1, parseCalls);
}
private Task CreateEmptyPackage(string filePath)
{
using var fileStream = File.Create(filePath);
using var archive = new ZipArchive(fileStream, ZipArchiveMode.Create);
// Create empty archive
return Task.CompletedTask;
}
public void Dispose()