Improves package download and caching
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 53s
Build and Push Docker Images / build (src/LiquidCode.Tester.Worker/Dockerfile, git.nullptr.top/liquidcode/liquidcode-tester-worker-roman, worker) (push) Successful in 50s
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 53s
Build and Push Docker Images / build (src/LiquidCode.Tester.Worker/Dockerfile, git.nullptr.top/liquidcode/liquidcode-tester-worker-roman, worker) (push) Successful in 50s
Adds package caching to reduce download frequency. Introduces a `PackageDownloadService` with memory caching to store downloaded packages, identified by mission ID, for reuse. Uses concurrent locks to prevent race conditions during download. Modifies the worker client service to optionally delete the package after sending, allowing cached packages to be retained.
This commit is contained in:
@@ -30,11 +30,11 @@ public class TesterController : ControllerBase
|
||||
|
||||
try
|
||||
{
|
||||
// Download the package
|
||||
var packagePath = await _packageDownloadService.DownloadPackageAsync(request.PackageUrl);
|
||||
// Download the package or use cached version if available
|
||||
var packagePath = await _packageDownloadService.GetOrDownloadPackageAsync(request.MissionId, request.PackageUrl);
|
||||
|
||||
// Send to appropriate worker based on language
|
||||
await _workerClientService.SendToWorkerAsync(request, packagePath);
|
||||
await _workerClientService.SendToWorkerAsync(request, packagePath, deletePackageAfterSend: false);
|
||||
|
||||
return Accepted(new { message = "Submit accepted for testing", submitId = request.Id });
|
||||
}
|
||||
@@ -86,7 +86,7 @@ public class TesterController : ControllerBase
|
||||
);
|
||||
|
||||
// Send to appropriate worker based on language
|
||||
await _workerClientService.SendToWorkerAsync(submitModel, packagePath);
|
||||
await _workerClientService.SendToWorkerAsync(submitModel, packagePath, deletePackageAfterSend: true);
|
||||
|
||||
return Accepted(new { message = "Submit accepted for testing", submitId = request.Id });
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ builder.Services.AddOpenApi();
|
||||
|
||||
// Add HttpClient
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddMemoryCache();
|
||||
|
||||
// Register application services
|
||||
builder.Services.AddSingleton<IPackageDownloadService, PackageDownloadService>();
|
||||
|
||||
@@ -3,9 +3,10 @@ namespace LiquidCode.Tester.Gateway.Services;
|
||||
public interface IPackageDownloadService
|
||||
{
|
||||
/// <summary>
|
||||
/// Downloads a package from the specified URL
|
||||
/// Retrieves a cached package for the mission or downloads it if missing.
|
||||
/// </summary>
|
||||
/// <param name="packageUrl">URL to download the package from</param>
|
||||
/// <returns>Path to the downloaded package file</returns>
|
||||
Task<string> DownloadPackageAsync(string packageUrl);
|
||||
/// <param name="missionId">Unique mission identifier used as cache key.</param>
|
||||
/// <param name="packageUrl">URL to download the package from when cache is cold.</param>
|
||||
/// <returns>Path to the cached or downloaded package file.</returns>
|
||||
Task<string> GetOrDownloadPackageAsync(long missionId, string packageUrl);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ public interface IWorkerClientService
|
||||
/// 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);
|
||||
/// <param name="packagePath">Local path to the package that will be streamed to the worker</param>
|
||||
/// <param name="deletePackageAfterSend">Indicates whether the package file should be removed after upload</param>
|
||||
Task SendToWorkerAsync(SubmitForTesterModel submit, string packagePath, bool deletePackageAfterSend);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace LiquidCode.Tester.Gateway.Services;
|
||||
|
||||
public class PackageDownloadService : IPackageDownloadService
|
||||
@@ -5,14 +8,20 @@ public class PackageDownloadService : IPackageDownloadService
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILogger<PackageDownloadService> _logger;
|
||||
private readonly string _downloadDirectory;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly ConcurrentDictionary<long, SemaphoreSlim> _locks = new();
|
||||
|
||||
private sealed record PackageCacheEntry(string FilePath);
|
||||
|
||||
public PackageDownloadService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILogger<PackageDownloadService> logger,
|
||||
IConfiguration configuration)
|
||||
IConfiguration configuration,
|
||||
IMemoryCache memoryCache)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_logger = logger;
|
||||
_memoryCache = memoryCache;
|
||||
_downloadDirectory = configuration["PackageDownloadDirectory"] ?? Path.Combine(Path.GetTempPath(), "packages");
|
||||
|
||||
if (!Directory.Exists(_downloadDirectory))
|
||||
@@ -21,12 +30,25 @@ public class PackageDownloadService : IPackageDownloadService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> DownloadPackageAsync(string packageUrl)
|
||||
public async Task<string> GetOrDownloadPackageAsync(long missionId, string packageUrl)
|
||||
{
|
||||
_logger.LogInformation("Downloading package from {Url}", packageUrl);
|
||||
if (TryGetCachedFile(missionId, out var cachedFile))
|
||||
{
|
||||
return cachedFile;
|
||||
}
|
||||
|
||||
var missionLock = _locks.GetOrAdd(missionId, _ => new SemaphoreSlim(1, 1));
|
||||
|
||||
await missionLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (TryGetCachedFile(missionId, out cachedFile))
|
||||
{
|
||||
return cachedFile;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Downloading package for mission {MissionId} from {Url}", missionId, packageUrl);
|
||||
|
||||
var httpClient = _httpClientFactory.CreateClient();
|
||||
var response = await httpClient.GetAsync(packageUrl);
|
||||
response.EnsureSuccessStatusCode();
|
||||
@@ -34,16 +56,92 @@ public class PackageDownloadService : IPackageDownloadService
|
||||
var fileName = $"package_{Guid.NewGuid()}.zip";
|
||||
var filePath = Path.Combine(_downloadDirectory, fileName);
|
||||
|
||||
try
|
||||
{
|
||||
await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
await response.Content.CopyToAsync(fileStream);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
catch (Exception cleanupEx)
|
||||
{
|
||||
_logger.LogWarning(cleanupEx, "Failed to clean up temporary file {Path} after download error", filePath);
|
||||
}
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
CacheFile(missionId, filePath);
|
||||
|
||||
_logger.LogInformation("Package downloaded and cached for mission {MissionId} at {Path}", missionId, filePath);
|
||||
|
||||
_logger.LogInformation("Package downloaded successfully to {Path}", filePath);
|
||||
return filePath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to download package from {Url}", packageUrl);
|
||||
_logger.LogError(ex, "Failed to download package for mission {MissionId} from {Url}", missionId, packageUrl);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
missionLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetCachedFile(long missionId, out string filePath)
|
||||
{
|
||||
if (_memoryCache.TryGetValue(missionId, out PackageCacheEntry? cacheEntry))
|
||||
{
|
||||
if (cacheEntry is not null && File.Exists(cacheEntry.FilePath))
|
||||
{
|
||||
filePath = cacheEntry.FilePath;
|
||||
_logger.LogInformation("Using cached package for mission {MissionId} from {Path}", missionId, filePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
_memoryCache.Remove(missionId);
|
||||
}
|
||||
|
||||
filePath = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void CacheFile(long missionId, string filePath)
|
||||
{
|
||||
var cacheEntry = new PackageCacheEntry(filePath);
|
||||
var options = new MemoryCacheEntryOptions
|
||||
{
|
||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
|
||||
};
|
||||
|
||||
options.RegisterPostEvictionCallback((_, value, _, _) =>
|
||||
{
|
||||
if (value is not PackageCacheEntry entry)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(entry.FilePath))
|
||||
{
|
||||
File.Delete(entry.FilePath);
|
||||
_logger.LogInformation("Removed cached package file {Path} after expiration", entry.FilePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to delete cached package file {Path} during eviction", entry.FilePath);
|
||||
}
|
||||
});
|
||||
|
||||
_memoryCache.Set(missionId, cacheEntry, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public class WorkerClientService : IWorkerClientService
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public async Task SendToWorkerAsync(SubmitForTesterModel submit, string packagePath)
|
||||
public async Task SendToWorkerAsync(SubmitForTesterModel submit, string packagePath, bool deletePackageAfterSend)
|
||||
{
|
||||
var workerUrl = GetWorkerUrlForLanguage(submit.Language);
|
||||
_logger.LogInformation("Sending submit {SubmitId} to worker at {WorkerUrl}", submit.Id, workerUrl);
|
||||
@@ -56,7 +56,9 @@ public class WorkerClientService : IWorkerClientService
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Clean up downloaded package
|
||||
if (deletePackageAfterSend)
|
||||
{
|
||||
// Clean up package file when it is not needed anymore
|
||||
try
|
||||
{
|
||||
if (File.Exists(packagePath))
|
||||
@@ -70,6 +72,7 @@ public class WorkerClientService : IWorkerClientService
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetWorkerUrlForLanguage(string language)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user