Skip to content

Commit da21dc4

Browse files
authored
Allow serving on missing dependency errors (#617)
1 parent a005b0c commit da21dc4

File tree

7 files changed

+137
-1
lines changed

7 files changed

+137
-1
lines changed

examples/docker/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ services:
1212
- Registry:RegistryFilePath=/data/registry.json # Path to the file (relative or absolute) where the packages registry file will be stored, default is "registry.json".
1313
- Registry:RootPersistentFolder=/data/unity_packages # Path to the folder (relative or absolute) where the packages cache will be stored, default is "unity_packages".
1414
- Registry:UpdateInterval=00:01:00 # Packages update interval, default is "00:10:00" (10 minutes).
15+
- Registry:AllowServingWithMissingDependencies=false # Set to true to ignore missing dependency errors and keep serving (friendlier for production).
1516
- Logging:LogLevel:Default=Information
1617
ports:
1718
- 5000:8080
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Net;
6+
using System.Net.Http;
7+
using System.Text.Json;
8+
using System.Threading.Tasks;
9+
using Microsoft.AspNetCore.Hosting;
10+
using Microsoft.AspNetCore.Mvc.Testing;
11+
using Microsoft.Extensions.Configuration;
12+
using Microsoft.Extensions.DependencyInjection;
13+
using NUnit.Framework;
14+
using UnityNuGet;
15+
using UnityNuGet.Npm;
16+
17+
namespace UnityNuGet.Server.Tests
18+
{
19+
public class AllowServingWithMissingDependenciesTests
20+
{
21+
private readonly AllowServingWithMissingDependenciesWebApplicationFactory _webApplicationFactory;
22+
23+
public AllowServingWithMissingDependenciesTests()
24+
{
25+
_webApplicationFactory = new AllowServingWithMissingDependenciesWebApplicationFactory();
26+
}
27+
28+
[OneTimeTearDown]
29+
public void OneTimeTearDown()
30+
{
31+
_webApplicationFactory.Dispose();
32+
}
33+
34+
[Test]
35+
public async Task GetAll_Succeeds_WhenBuildHasErrors()
36+
{
37+
using HttpClient httpClient = _webApplicationFactory.CreateDefaultClient();
38+
39+
await WaitForInitialization(_webApplicationFactory.Services, TimeSpan.FromMinutes(2));
40+
41+
RegistryCacheReport registryCacheReport = _webApplicationFactory.Services.GetRequiredService<RegistryCacheReport>();
42+
Assert.That(registryCacheReport.ErrorMessages.Any(), Is.True, "Expected build errors but none were reported.");
43+
44+
HttpResponseMessage response = await httpClient.GetAsync("/-/all");
45+
46+
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
47+
48+
string responseContent = await response.Content.ReadAsStringAsync();
49+
NpmPackageListAllResponse npmPackageListAllResponse = JsonSerializer.Deserialize(
50+
responseContent,
51+
UnityNuGetJsonSerializerContext.Default.NpmPackageListAllResponse)!;
52+
53+
Assert.That(npmPackageListAllResponse, Is.Not.Null);
54+
}
55+
56+
private static async Task WaitForInitialization(IServiceProvider serviceProvider, TimeSpan timeout)
57+
{
58+
RegistryCacheSingleton registryCacheSingleton = serviceProvider.GetRequiredService<RegistryCacheSingleton>();
59+
60+
DateTime deadline = DateTime.UtcNow.Add(timeout);
61+
62+
while (registryCacheSingleton.Instance == null)
63+
{
64+
if (DateTime.UtcNow >= deadline)
65+
{
66+
Assert.Fail("Timed out waiting for RegistryCache initialization.");
67+
}
68+
69+
await Task.Delay(50);
70+
}
71+
}
72+
}
73+
74+
internal class AllowServingWithMissingDependenciesWebApplicationFactory : WebApplicationFactory<Program>
75+
{
76+
private readonly string _rootPersistentFolder;
77+
private readonly string _registryFilePath;
78+
79+
public AllowServingWithMissingDependenciesWebApplicationFactory()
80+
{
81+
_rootPersistentFolder = Path.Combine(Path.GetTempPath(), "UnityNuGet.Server.Tests", Guid.NewGuid().ToString("N"));
82+
Directory.CreateDirectory(_rootPersistentFolder);
83+
84+
_registryFilePath = Path.Combine(AppContext.BaseDirectory, "registry.test.missing-dep.json");
85+
}
86+
87+
protected override void ConfigureWebHost(IWebHostBuilder builder)
88+
{
89+
base.ConfigureWebHost(builder);
90+
91+
builder.ConfigureAppConfiguration(configBuilder =>
92+
{
93+
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
94+
{
95+
{ WebHostDefaults.ServerUrlsKey, "http://localhost" },
96+
{ "Registry:RegistryFilePath", _registryFilePath },
97+
{ "Registry:RootPersistentFolder", _rootPersistentFolder },
98+
{ "Registry:AllowServingWithMissingDependencies", "true" },
99+
{ "Registry:Filter", "Serilog.Sinks.File" }
100+
});
101+
});
102+
}
103+
}
104+
}

src/UnityNuGet.Server.Tests/UnityNuGet.Server.Tests.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,10 @@
2121
<ProjectReference Include="..\UnityNuGet.Server\UnityNuGet.Server.csproj" />
2222
</ItemGroup>
2323

24+
<ItemGroup>
25+
<None Include="registry.test.missing-dep.json">
26+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
27+
</None>
28+
</ItemGroup>
29+
2430
</Project>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"Serilog.Sinks.File": {
3+
"listed": true,
4+
"version": "5.0.0"
5+
}
6+
}

src/UnityNuGet.Server/RegistryCacheUpdater.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ internal sealed class RegistryCacheUpdater(Registry registry, RegistryCacheRepor
1818
private readonly RegistryCacheSingleton _currentRegistryCache = currentRegistryCache;
1919
private readonly ILogger _logger = logger;
2020
private readonly RegistryOptions _registryOptions = registryOptionsAccessor.Value;
21+
private readonly bool _allowServingWithMissingDependencies = registryOptionsAccessor.Value.AllowServingWithMissingDependencies;
2122

2223
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
2324
{
@@ -57,7 +58,16 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
5758

5859
if (_registryCacheReport.ErrorMessages.Any())
5960
{
60-
_logger.LogInformation("RegistryCache not updated due to errors. See previous logs");
61+
bool onlyMissingDependencyErrors = _registryCacheReport.ErrorMessages.All(IsMissingDependencyError);
62+
if (_allowServingWithMissingDependencies && onlyMissingDependencyErrors)
63+
{
64+
_currentRegistryCache.Instance = newRegistryCache;
65+
_logger.LogWarning("RegistryCache updated with errors. Serving partial results; see previous logs.");
66+
}
67+
else
68+
{
69+
_logger.LogInformation("RegistryCache not updated due to errors. See previous logs");
70+
}
6171
}
6272
else
6373
{
@@ -91,5 +101,11 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
91101
_registryCacheReport.Complete();
92102
}
93103
}
104+
105+
private static bool IsMissingDependencyError(string message)
106+
{
107+
return message.Contains("has a dependency on", StringComparison.Ordinal)
108+
&& message.Contains("which is not in the registry", StringComparison.Ordinal);
109+
}
94110
}
95111
}

src/UnityNuGet.Server/appsettings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"RegistryFilePath": "registry.json",
1111
"RootPersistentFolder": "unity_packages",
1212
"UpdateInterval": "00:10:00",
13+
"AllowServingWithMissingDependencies": false,
1314
"TargetFrameworks": [
1415
{
1516
"Name": "netstandard2.0",

src/UnityNuGet/RegistryOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ public class RegistryOptions
3535
[Required]
3636
public TimeSpan UpdateInterval { get; set; }
3737

38+
public bool AllowServingWithMissingDependencies { get; set; }
39+
3840
[Required]
3941
[ValidateEnumeratedItems]
4042
public RegistryTargetFramework[]? TargetFrameworks { get; set; }

0 commit comments

Comments
 (0)