feat(audit): production gRPC IPullAuditEventsClient for site reconciliation

This commit is contained in:
Joseph Doherty
2026-06-15 09:41:13 -04:00
parent 9aa1259504
commit 2adc5767da
3 changed files with 488 additions and 0 deletions
@@ -362,4 +362,69 @@ public static class ServiceCollectionExtensions
return services;
}
/// <summary>
/// Audit Log (#23) M6 — central-only registration of the production
/// <see cref="IPullAuditEventsClient"/> (<see cref="GrpcPullAuditEventsClient"/>)
/// and its unary-call invoker (<see cref="GrpcPullAuditEventsInvoker"/>) used
/// by <see cref="SiteAuditReconciliationActor"/> to pull reconciliation
/// batches from each site over the <c>PullAuditEvents</c> gRPC RPC.
/// </summary>
/// <remarks>
/// <para>
/// Kept out of <see cref="AddAuditLog"/> — which also runs on site
/// composition roots — because the client dials sites and resolves
/// <see cref="ISiteEnumerator"/> (a central-only collaborator wired
/// alongside the reconciliation singleton). Folding it into
/// <see cref="AddAuditLog"/> would register a site-dialing client on every
/// site host, violating the "every <c>Add*</c> call is safe from any
/// composition root" invariant. This helper is the central analogue of
/// <see cref="AddAuditLogCentralMaintenance"/>.
/// </para>
/// <para>
/// The <see cref="GrpcPullAuditEventsInvoker"/> binds with default
/// <see cref="ZB.MOM.WW.ScadaBridge.Communication.CommunicationOptions"/>
/// keepalive unless an <c>IOptions&lt;CommunicationOptions&gt;</c> is
/// already registered, in which case the configured timings flow through —
/// matching how <c>SiteStreamGrpcClientFactory</c> takes its keepalive from
/// the same options.
/// </para>
/// <para>
/// <see cref="ISiteEnumerator"/> is NOT registered here: its production
/// implementation (wrapping <c>ISiteRepository</c>) ships with the
/// reconciliation-singleton wiring in the Host. The client resolves the
/// enumerator lazily at actor-construction time, so this binding is safe to
/// issue before the enumerator binding lands.
/// </para>
/// </remarks>
/// <param name="services">The service collection to register into.</param>
/// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
public static IServiceCollection AddAuditLogCentralReconciliationClient(
this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
// The invoker owns the per-endpoint GrpcChannel cache, so it must be a
// singleton — a fresh invoker per resolution would leak channels.
// Resolve CommunicationOptions if present (the central Host binds it),
// otherwise fall back to defaults so this helper stays standalone.
services.TryAddSingleton<GrpcPullAuditEventsInvoker>(sp =>
{
var options = sp
.GetService<Microsoft.Extensions.Options.IOptions<
ZB.MOM.WW.ScadaBridge.Communication.CommunicationOptions>>();
return options is null
? new GrpcPullAuditEventsInvoker()
: new GrpcPullAuditEventsInvoker(options.Value);
});
services.TryAddSingleton<GrpcPullAuditEventsClient.IPullAuditEventsInvoker>(
sp => sp.GetRequiredService<GrpcPullAuditEventsInvoker>());
services.TryAddSingleton<IPullAuditEventsClient>(sp => new GrpcPullAuditEventsClient(
sp.GetRequiredService<ISiteEnumerator>(),
sp.GetRequiredService<GrpcPullAuditEventsClient.IPullAuditEventsInvoker>(),
sp.GetRequiredService<ILogger<GrpcPullAuditEventsClient>>()));
return services;
}
}