-
Notifications
You must be signed in to change notification settings - Fork 886
[5752] FakeLogCollector waiting capabilities #6228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Demo30
merged 52 commits into
dotnet:main
from
Demo30:twiesner/5752_FakeLogCollector_waiting_differentDesign
Jan 21, 2026
Merged
Changes from all commits
Commits
Show all changes
52 commits
Select commit
Hold shift + click to select a range
c36f285
wip
c248a7c
Merge remote-tracking branch 'orig-dotnet-extensions/main' into twies…
1b4ccdf
refactor
2ae8401
Merge remote-tracking branch 'orig-dotnet-extensions/main' into twies…
721ce3e
we might actually need timeout too
233e23a
WIP
38c1d9a
timeout capability
84a5a3c
separate test cases into separate tests
32d426e
cleanup
27142fb
Merge remote-tracking branch 'orig-dotnet-extensions/main' into twies…
1445427
Merge remote-tracking branch 'orig-dotnet-extensions/main'
26c6852
Merge branch 'main' into twiesner/5752_FakeLogCollector_waiting_diffe…
562db95
cleanup
aa9be3f
stackallock threshold + test run infrastructure timeout
9332616
fake time provider based tests instead of real time
45d9eaf
enriching test cases by logs before awaiting and between interesting …
4dec848
Merge remote-tracking branch 'dotnet-extensions/main' into twiesner/5…
985ed46
continue with enumeration
5ece729
wip
64a2775
wip
da4dc79
wip
efc4324
Merge branch 'twiesner/5752_refactors' into twiesner/5752_FakeLogColl…
367bae6
removing previous implementation of easier review
eb342f3
old waiting logic removal
7c4b5d6
minor test cleanup
a92bf7a
using retrieved index in test
a6f4f2e
guarding against concurrent movenextasync calls
1cc522a
minor tweaks
9c79cda
wip
b2fe7ee
adjust enumerator index on clear
1c4c5a9
demo tests
3c9c668
extend demo test to better illustrate
2d95552
remove timeout param
75b5153
remove startingIndex param support
77d6993
discarding the Clear(count) functionality
fd74ce6
count param
9286930
issue with concurrent Clear call and maxItems
c1baae9
count removed
9a358ce
minor changes
255ac3e
Merge remote-tracking branch 'dotnet-extensions/main' into twiesner/5…
62152ac
fixes
0abc130
test
c31730b
Update test/Libraries/Microsoft.Extensions.Diagnostics.Testing.Tests/…
Demo30 64b63df
Update src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging…
Demo30 b19b719
modified message
5c40845
checking disposal in thread-safe manner
3e692b7
fixed interlocked for net462 target; volatile reading of _disposed
b489573
volatile read on _recordCollectionVersion field
6c22cd6
_recordCollectionVersion volatile everywhere
5b5af68
Clear during wait test
fb1aa79
clearing up analyzer warnings
0fadd0a
reliable tests
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
214 changes: 214 additions & 0 deletions
214
...aries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogCollector.LogEnumeration.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,214 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.Shared.DiagnosticIds; | ||
|
|
||
| namespace Microsoft.Extensions.Logging.Testing; | ||
|
|
||
| public partial class FakeLogCollector | ||
| { | ||
| private volatile int _recordCollectionVersion; | ||
|
|
||
| private TaskCompletionSource<object?> _logEnumerationSharedWaiter = | ||
| new(TaskCreationOptions.RunContinuationsAsynchronously); | ||
|
|
||
| private int _waitingEnumeratorCount; | ||
|
|
||
| /// <summary> | ||
| /// Asynchronously enumerates the <see cref="FakeLogRecord"/> instances collected by this <see cref="FakeLogCollector"/>. | ||
| /// </summary> | ||
| /// <param name="cancellationToken"> | ||
| /// A token that can be used to cancel the asynchronous enumeration. This token is observed while creating | ||
| /// and iterating the asynchronous sequence. | ||
| /// </param> | ||
| /// <returns> | ||
| /// An <see cref="IAsyncEnumerable{T}"/> that yields <see cref="FakeLogRecord"/> instances as they are written. | ||
| /// The sequence does not have a completion state defined and awaits subsequent elements indefinitely, | ||
| /// or stops when cancellation is requested. | ||
| /// </returns> | ||
| /// <remarks> | ||
| /// The returned sequence is <c>hot</c>: it streams log records as they become available and may block between | ||
| /// elements while waiting for additional logs to be written. Multiple independent enumerations can be created | ||
| /// by calling this method multiple times. | ||
| /// </remarks> | ||
| /// <exception cref="OperationCanceledException"> | ||
| /// Thrown when the provided <paramref name="cancellationToken"/> or the enumerator's own cancellation token | ||
| /// is canceled while waiting for the next log record. | ||
| /// </exception> | ||
| /// <example> | ||
| /// The following example shows how to consume logs asynchronously: | ||
| /// <code language="csharp"><![CDATA[ | ||
| /// var collector = new FakeLogCollector(); | ||
| /// using var cts = new CancellationTokenSource(); | ||
| /// | ||
| /// await foreach (var record in collector.GetLogsAsync(cts.Token)) | ||
| /// { | ||
| /// Console.WriteLine($"{record.Level}: {record.Message}"); | ||
| /// } | ||
| /// ]]></code> | ||
| /// </example> | ||
| [Experimental(DiagnosticIds.Experiments.Telemetry)] | ||
| public IAsyncEnumerable<FakeLogRecord> GetLogsAsync(CancellationToken cancellationToken = default) | ||
| => new LogAsyncEnumerable(this, cancellationToken); | ||
|
|
||
| private sealed class LogAsyncEnumerable : IAsyncEnumerable<FakeLogRecord> | ||
| { | ||
| private readonly FakeLogCollector _collector; | ||
| private readonly CancellationToken _enumerableCancellationToken; | ||
|
|
||
| internal LogAsyncEnumerable( | ||
| FakeLogCollector collector, | ||
| CancellationToken enumerableCancellationToken) | ||
| { | ||
| _collector = collector; | ||
| _enumerableCancellationToken = enumerableCancellationToken; | ||
| } | ||
|
|
||
| public IAsyncEnumerator<FakeLogRecord> GetAsyncEnumerator( | ||
| CancellationToken enumeratorCancellationToken = default) | ||
| => new StreamEnumerator(_collector, _enumerableCancellationToken, enumeratorCancellationToken); | ||
| } | ||
|
|
||
| private sealed class StreamEnumerator : IAsyncEnumerator<FakeLogRecord> | ||
| { | ||
| private readonly FakeLogCollector _collector; | ||
| private readonly CancellationTokenSource _mainCts; | ||
| private int _index; | ||
| private int _disposed; // 0 = false, 1 = true (int type used for net462 compatibility) | ||
| private int _observedRecordCollectionVersion; | ||
|
|
||
| // Concurrent MoveNextAsync guard | ||
| private int _moveNextActive; // 0 = inactive, 1 = active (int type used for net462 compatibility) | ||
|
|
||
| public StreamEnumerator( | ||
| FakeLogCollector collector, | ||
| CancellationToken enumerableCancellationToken, | ||
| CancellationToken enumeratorCancellationToken) | ||
| { | ||
| _collector = collector; | ||
| _mainCts = enumerableCancellationToken.CanBeCanceled || enumeratorCancellationToken.CanBeCanceled | ||
| ? CancellationTokenSource.CreateLinkedTokenSource(enumerableCancellationToken, enumeratorCancellationToken) | ||
| : new CancellationTokenSource(); | ||
| _observedRecordCollectionVersion = collector._recordCollectionVersion; | ||
|
Demo30 marked this conversation as resolved.
Demo30 marked this conversation as resolved.
|
||
| } | ||
|
|
||
| public FakeLogRecord Current | ||
| { | ||
| get => field ?? throw new InvalidOperationException("Enumeration not started."); | ||
| private set; | ||
| } | ||
|
Demo30 marked this conversation as resolved.
|
||
|
|
||
| public async ValueTask<bool> MoveNextAsync() | ||
| { | ||
| if (Interlocked.CompareExchange(ref _moveNextActive, 1, 0) == 1) | ||
| { | ||
| throw new InvalidOperationException("MoveNextAsync is already in progress. Concurrent calls are not allowed."); | ||
| } | ||
|
|
||
| try | ||
| { | ||
| ThrowIfDisposed(); | ||
|
|
||
| var mainCancellationToken = _mainCts.Token; | ||
|
|
||
| mainCancellationToken.ThrowIfCancellationRequested(); | ||
|
|
||
| while (true) | ||
| { | ||
| TaskCompletionSource<object?>? waiter = null; | ||
|
|
||
| try | ||
| { | ||
| mainCancellationToken.ThrowIfCancellationRequested(); | ||
|
|
||
| lock (_collector._records) | ||
| { | ||
| int currentVersion = _collector._recordCollectionVersion; | ||
| if (_observedRecordCollectionVersion != currentVersion) | ||
| { | ||
| _index = 0; // based on assumption that version changed on full collection clear | ||
| _observedRecordCollectionVersion = currentVersion; | ||
| } | ||
|
|
||
| if (_index < _collector._records.Count) | ||
| { | ||
| Current = _collector._records[_index++]; | ||
| return true; | ||
| } | ||
|
|
||
| // waiter needs to be subscribed within records lock | ||
| // if not: more records could be added in the meantime and the waiter could be stuck waiting even though the index is behind the actual count | ||
| waiter = _collector._logEnumerationSharedWaiter; | ||
| _collector._waitingEnumeratorCount++; | ||
| } | ||
|
|
||
| // After the wait is complete in normal flow, no need to decrement because the shared waiter will be swapped and counter reset. | ||
| _ = await waiter.Task.WaitAsync(mainCancellationToken).ConfigureAwait(false); | ||
| } | ||
| catch (OperationCanceledException) | ||
| { | ||
| if (waiter is not null) | ||
| { | ||
| lock (_collector._records) | ||
| { | ||
| if ( | ||
| _collector._waitingEnumeratorCount > 0 // counter can be zero during the cancellation path | ||
| && waiter == _collector._logEnumerationSharedWaiter // makes sure we adjust the counter for the same shared waiting session | ||
| ) | ||
| { | ||
| _collector._waitingEnumeratorCount--; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| throw; | ||
| } | ||
| } | ||
|
|
||
| } | ||
| finally | ||
| { | ||
| Volatile.Write(ref _moveNextActive, 0); | ||
| } | ||
| } | ||
|
|
||
| #if !NETCOREAPP | ||
| public ValueTask DisposeAsync() | ||
| { | ||
| if (Interlocked.Exchange(ref _disposed, 1) == 1) | ||
| { | ||
| return default; | ||
| } | ||
|
|
||
| _mainCts.Cancel(); | ||
| _mainCts.Dispose(); | ||
|
|
||
| return default; | ||
|
Demo30 marked this conversation as resolved.
|
||
| } | ||
| #else | ||
| public async ValueTask DisposeAsync() | ||
| { | ||
| if (Interlocked.Exchange(ref _disposed, 1) == 1) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| await _mainCts.CancelAsync().ConfigureAwait(false); | ||
| _mainCts.Dispose(); | ||
| } | ||
| #endif | ||
|
|
||
| private void ThrowIfDisposed() | ||
| { | ||
| if (Volatile.Read(ref _disposed) == 1) | ||
| { | ||
| throw new ObjectDisposedException(nameof(StreamEnumerator)); | ||
| } | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.