| File: Diagnostics\OperationEventSource.cs | Web Access |
| Project: src\src\Aspire.Hosting.Native.Core\Aspire.Hosting.Native.Core.csproj (Aspire.Hosting.Native.Core) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading.Channels; using System.Diagnostics; namespace Aspire.Hosting.Native.Diagnostics; /// <summary>Fans out diagnostics without running consumer code on the producer.</summary> internal sealed class OperationEventSource : IDisposable { private readonly Lock _gate = new(); private readonly HashSet<OperationEventSubscription> _subscriptions = []; private bool _disposed; public OperationEventSubscription Subscribe(int capacity = 1024, ActivityTraceId? traceId = null) { ArgumentOutOfRangeException.ThrowIfLessThan(capacity, 1); ArgumentOutOfRangeException.ThrowIfGreaterThan(capacity, 65536); lock (_gate) { ObjectDisposedException.ThrowIf(_disposed, this); var subscription = new OperationEventSubscription(this, capacity, traceId); _subscriptions.Add(subscription); return subscription; } } public void Publish(OperationEvent value) { lock (_gate) { foreach (var subscription in _subscriptions.ToArray()) { if (subscription.TraceId is { } traceId && !string.Equals(value.TraceId, traceId, StringComparison.Ordinal)) { continue; } // Wait mode makes TryWrite report overflow instead of silently // dropping events. Only the lagging subscriber loses the feed; // its retained prefix drains before the classified error. if (!subscription.Writer.TryWrite(value)) { _subscriptions.Remove(subscription); subscription.Writer.TryComplete(new DiagnosticOverflowException()); } } } } internal void Unsubscribe(OperationEventSubscription subscription) { lock (_gate) { _subscriptions.Remove(subscription); subscription.Writer.TryComplete(); } } public void Dispose() { lock (_gate) { _disposed = true; foreach (var subscription in _subscriptions) { subscription.Writer.TryComplete(); } _subscriptions.Clear(); } } } /// <summary>Owns one bounded diagnostic queue; disposal completes its retained prefix.</summary> internal sealed class OperationEventSubscription : IDisposable { private readonly OperationEventSource _owner; private readonly Channel<OperationEvent> _channel; internal OperationEventSubscription(OperationEventSource owner, int capacity, ActivityTraceId? traceId) { _owner = owner; TraceId = traceId?.ToString(); _channel = Channel.CreateBounded<OperationEvent>(new BoundedChannelOptions(capacity) { FullMode = BoundedChannelFullMode.Wait, AllowSynchronousContinuations = false }); } public ChannelReader<OperationEvent> Reader => _channel.Reader; internal string? TraceId { get; } internal ChannelWriter<OperationEvent> Writer => _channel.Writer; public void Dispose() => _owner.Unsubscribe(this); } /// <summary>Reports a diagnostic subscriber exceeding its independent queue capacity.</summary> internal sealed class DiagnosticOverflowException() : InvalidOperationException("The diagnostic subscriber exceeded its queue capacity.");