Compare commits

...
Author SHA1 Message Date
KyuubiYoru dc2de76963 feat(server): answer the root path and probe container health
A bare GET / previously returned an empty 404, which reads as an outage
even while the service is healthy. The root now redirects to
/diagnostics when the dashboard is enabled and otherwise returns a
small JSON status pointing at /health/live. The route is excluded from
the OpenAPI description, so the frozen v1 surface is unchanged.

The chiseled runtime image ships no shell or curl, so the container
healthcheck re-enters the server assembly with --health-probe, which
queries the local liveness endpoint and reports through the exit code.
Dockerfile and compose both wire the probe (30s interval, 3 retries).
2026-08-31 03:55:17 +02:00
KyuubiYoru 13382531ce docs(integration): correct channel claim and flag UDP port collision (#21)
quality-gate / container (push) Successful in 1m43s
quality-gate / quality (push) Successful in 2m43s
SpaceGame's v1.0.0 evaluation (SpaceGame docs/rendezvous-eval.md @ 8eb5607) found the pilot doc asserting ChannelsCount=3 as current SpaceGame fact — their source uses LiteNetLib's single-channel default, and a peer mismatch fails connections silently, so the doc now states the requirement as agreement rather than a count. The compose file also gains a loud warning that host UDP 9050 collides with common game-client defaults during same-machine evaluation.
2026-08-22 23:21:10 +02:00
7 changed files with 89 additions and 2 deletions
+4
View File
@@ -32,4 +32,8 @@ COPY --from=build --chown=1654:1654 /out/ ./
USER 1654:1654
EXPOSE 8080/tcp
EXPOSE 9050/udp
# The chiseled image has no shell or curl; the probe re-enters the server
# assembly in --health-probe mode and reports through the exit code.
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD ["dotnet", "FinalFactory.Rendezvous.Server.dll", "--health-probe"]
ENTRYPOINT ["dotnet", "FinalFactory.Rendezvous.Server.dll"]
+10
View File
@@ -24,6 +24,12 @@ services:
hard: 4096
stop_grace_period: 40s
restart: unless-stopped
healthcheck:
test: ["CMD", "dotnet", "FinalFactory.Rendezvous.Server.dll", "--health-probe"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
environment:
ASPNETCORE_ENVIRONMENT: Production
ASPNETCORE_HTTP_PORTS: "8080"
@@ -34,4 +40,8 @@ services:
- ${RENDEZVOUS_SECRET_SOURCE:-./secrets/signing-key}:/run/secrets/rendezvous-signing-key:ro
ports:
- "127.0.0.1:${RENDEZVOUS_HTTP_HOST_PORT:-8080}:8080/tcp"
# 9050 is also a common game-client default UDP port. Evaluating this
# service on the same machine as a running game client can silently
# collide; remap with RENDEZVOUS_UDP_HOST_PORT and keep the advertised
# Rendezvous:Deployment:PublicUdpPort in appsettings matched to it.
- "${RENDEZVOUS_UDP_HOST_PORT:-9050}:9050/udp"
+5 -2
View File
@@ -31,8 +31,11 @@ must be repeated from the public feed.
## Proven local path
The SpaceGame host and client each create one caller-owned `NetManager`, set its
three gameplay QoS channels before `Start`, and give the same manager and
The SpaceGame host and client each create one caller-owned `NetManager`,
configure any additional gameplay QoS channels before `Start` (the pilot
harness used three; current SpaceGame source uses LiteNetLib's default single
channel — both peers must simply agree, because a `ChannelsCount` mismatch
fails the connection silently), and give the same manager and
`RendezvousNetListener` to the coordinator. Rendezvous authenticates discovery,
join authorization, host presence, mediation, and connection outcome reporting.
After traversal, SpaceGame performs a separate audience-bound admission exchange
@@ -57,11 +57,13 @@
<Compile Include="Diagnostics/DiagnosticDashboardOptions.cs" />
<Compile Include="Http/ContractEndpoints.cs" />
<Compile Include="Http/RendezvousExceptionHandler.cs" />
<Compile Include="Http/RootEndpoints.cs" />
<Compile Include="JoinAttempts/JoinAttemptCursorCodec.cs" />
<Compile Include="JoinAttempts/JoinAttemptService.cs" />
<Compile Include="Observability/AuditOptions.cs" />
<Compile Include="Observability/AuditTrail.cs" />
<Compile Include="Observability/HealthEndpoints.cs" />
<Compile Include="Observability/HealthProbe.cs" />
<Compile Include="Observability/PrometheusMetricsEndpoint.cs" />
<Compile Include="Observability/RendezvousReadiness.cs" />
<Compile Include="Observability/RendezvousTelemetry.cs" />
@@ -0,0 +1,27 @@
using FinalFactory.Rendezvous.Contracts;
using FinalFactory.Rendezvous.Server.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace FinalFactory.Rendezvous.Server.Http;
internal static class RootEndpoints
{
public static IEndpointRouteBuilder MapRootEndpoint(
this IEndpointRouteBuilder endpoints)
{
endpoints.MapGet("/", ServeRoot).ExcludeFromDescription();
return endpoints;
}
private static IResult ServeRoot(
[FromServices] IOptions<DiagnosticDashboardOptions> diagnostics) =>
diagnostics.Value.Enabled
? Results.Redirect("/diagnostics")
: Results.Json(new
{
contractVersion = ContractLimits.ContractVersion,
service = "rendezvous",
health = "/health/live",
});
}
@@ -0,0 +1,34 @@
namespace FinalFactory.Rendezvous.Server.Observability;
/// <summary>
/// Container healthcheck entry mode (<c>--health-probe</c>). The chiseled
/// runtime image ships no shell or curl, so the probe re-enters the server
/// assembly, queries the local liveness endpoint, and reports via exit code.
/// </summary>
internal static class HealthProbe
{
public const string Argument = "--health-probe";
public static async Task<int> RunAsync()
{
string configuredPorts =
Environment.GetEnvironmentVariable("ASPNETCORE_HTTP_PORTS") ?? "8080";
string port = configuredPorts
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.FirstOrDefault() ?? "8080";
using HttpClient client = new() { Timeout = TimeSpan.FromSeconds(4) };
try
{
using HttpResponseMessage response = await client.GetAsync(
new Uri($"http://127.0.0.1:{port}/health/live"));
return response.IsSuccessStatusCode ? 0 : 1;
}
catch (Exception exception)
when (exception is HttpRequestException
or TaskCanceledException
or UriFormatException)
{
return 1;
}
}
}
@@ -16,6 +16,11 @@ using FinalFactory.Rendezvous.Server.Transport;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.OpenApi;
if (args.Contains(HealthProbe.Argument, StringComparer.Ordinal))
{
return await HealthProbe.RunAsync();
}
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Logging.AddFilter(
"Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware",
@@ -378,6 +383,7 @@ app.UseMiddleware<TelemetryMiddleware>();
app.UseExceptionHandler();
app.UseMiddleware<HttpAbuseProtectionMiddleware>();
app.MapOpenApi();
app.MapRootEndpoint();
app.MapRendezvousContractEndpoints();
app.MapOperatorEndpoints();
app.MapRendezvousHealthEndpoints();
@@ -385,6 +391,7 @@ app.MapDiagnosticDashboardEndpoints();
app.MapPrometheusMetricsEndpoint(metricsOptions, metricsCredential);
await app.RunAsync();
return 0;
/// <summary>
/// Entry point marker used by integration-test hosts.