diff --git a/src/Stl.Fusion.EntityFramework/DbEntityResolver.cs b/src/Stl.Fusion.EntityFramework/DbEntityResolver.cs index 8e3fad4c9..c42e40cbf 100644 --- a/src/Stl.Fusion.EntityFramework/DbEntityResolver.cs +++ b/src/Stl.Fusion.EntityFramework/DbEntityResolver.cs @@ -60,9 +60,11 @@ public record Options private ConcurrentDictionary>? _batchProcessors; private ITransientErrorDetector? _transientErrorDetector; + private ILogger? _log; protected Options Settings { get; } protected (Func> Query, int BatchSize)[] Queries { get; init; } + protected ILogger Log => _log ??= Services.LogFor(GetType()); public Func KeyExtractor { get; init; } public Expression> KeyExtractorExpression { get; init; } @@ -192,6 +194,7 @@ protected Func> CreateCompiledQu var batchProcessor = new BatchProcessor { BatchSize = Settings.BatchSize, Implementation = (batch, cancellationToken) => ProcessBatch(tenant, batch, cancellationToken), + Log = Log, }; Settings.ConfigureBatchProcessor?.Invoke(batchProcessor); if (batchProcessor.BatchSize != Settings.BatchSize) diff --git a/src/Stl.Testing/Output/TestOutputHelperAccessor.cs b/src/Stl.Testing/Output/TestOutputHelperAccessor.cs index 497e7857a..ed07ac041 100644 --- a/src/Stl.Testing/Output/TestOutputHelperAccessor.cs +++ b/src/Stl.Testing/Output/TestOutputHelperAccessor.cs @@ -3,10 +3,7 @@ namespace Stl.Testing.Output; -public class TestOutputHelperAccessor : ITestOutputHelperAccessor +public class TestOutputHelperAccessor(ITestOutputHelper? output) : ITestOutputHelperAccessor { - public ITestOutputHelper? Output { get; set; } - - public TestOutputHelperAccessor(ITestOutputHelper? output) - => Output = output; + public ITestOutputHelper? Output { get; set; } = output; } diff --git a/src/Stl/Async/BatchProcessor.cs b/src/Stl/Async/BatchProcessor.cs index f3e2a5e68..043393632 100644 --- a/src/Stl/Async/BatchProcessor.cs +++ b/src/Stl/Async/BatchProcessor.cs @@ -1,5 +1,4 @@ using Stl.Internal; -using Stl.OS; namespace Stl.Async; @@ -16,30 +15,25 @@ static file class BatchProcessor public class BatchProcessor(Channel.Item> queue) : ProcessorBase { - protected Channel Queue = queue; + private volatile IBatchProcessorWorkerPolicy _workerPolicy = BatchProcessorWorkerPolicy.Default; + + protected readonly Channel Queue = queue; protected int PlannedWorkerCount; protected HashSet Workers = new(); protected Task? WorkerCollectorTask; - protected object WorkerLock => Workers; - // Metrics - protected readonly object MetricsLock = new(); - protected int ProcessedItemCount; - protected long MinQueueDurationInTicks = long.MaxValue; - protected CpuTimestamp LastAdjustmentAt; + // Statistics + protected RingBuffer RecentReports = new(7); + protected CpuTimestamp CooldownEndsAt; // Settings - public int MinWorkerCount { get; set; } = 1; - public int MaxWorkerCount { get; set; } = HardwareInfo.GetProcessorCountFactor(2); - public int AdjustmentInterval { get; set; } = 16; - public TimeSpan AdjustmentPeriod { get; set; } = TimeSpan.FromMilliseconds(100); - public TimeSpan KillWorkerAt { get; set; } = TimeSpan.FromMilliseconds(1); - public TimeSpan Kill8WorkersAt { get; set; } = TimeSpan.FromMilliseconds(0.1); - public TimeSpan AddWorkerAt { get; set; } = TimeSpan.FromMilliseconds(20); - public TimeSpan Add4WorkersAt { get; set; } = TimeSpan.FromMilliseconds(100); - public TimeSpan WorkerCollectionPeriod { get; set; } = TimeSpan.FromSeconds(5); public int BatchSize { get; set; } = 256; public Func, CancellationToken, Task> Implementation { get; set; } = (_, _) => Task.CompletedTask; + public IBatchProcessorWorkerPolicy WorkerPolicy { + get => _workerPolicy; + set => Interlocked.Exchange(ref _workerPolicy, value); + } + public ILogger? Log { get; set; } public BatchProcessor() : this(Channel.CreateUnbounded(BatchProcessor.DefaultChannelOptions)) @@ -47,113 +41,104 @@ public BatchProcessor() protected override Task DisposeAsyncCore() { + // This method starts inside lock (Lock) block, so no need to lock here Queue.Writer.TryComplete(); - lock (WorkerLock) - return Workers.Count != 0 - ? Task.WhenAll(Workers.ToArray()) - : Task.CompletedTask; + return Workers.Count != 0 + ? Task.WhenAll(Workers.ToArray()) + : Task.CompletedTask; } - public async Task Process(T input, CancellationToken cancellationToken = default) + public Task Process(T input, CancellationToken cancellationToken = default) { if (PlannedWorkerCount == 0) { - lock (WorkerLock) - if (PlannedWorkerCount == 0 && !StopToken.IsCancellationRequested) - _ = UpdateWorkerCount(MinWorkerCount); + lock (Lock) + if (PlannedWorkerCount == 0 && !StopToken.IsCancellationRequested) { + var minWorkerCount = WorkerPolicy.MinWorkerCount; + if (minWorkerCount < 1) + throw Errors.InternalError("WorkerPolicy.MinWorkerCount < 1"); + _ = AddOrRemoveWorkers(WorkerPolicy.MinWorkerCount); + } } + var item = new Item(input, cancellationToken); - await Queue.Writer.WriteAsync(item, cancellationToken).ConfigureAwait(false); - var result = await item.ResultTask.ConfigureAwait(false); - var workerDelta = UpdateMetrics(item); - await UpdateWorkerCount(workerDelta).ConfigureAwait(false); - return result; + return Queue.Writer.TryWrite(item) + ? item.ResultTask + : ProcessAsync(item, cancellationToken); + + async Task ProcessAsync(Item item1, CancellationToken cancellationToken1) { + await Queue.Writer.WriteAsync(item1, cancellationToken1).ConfigureAwait(false); + return await item.ResultTask.ConfigureAwait(false); + } } public int GetWorkerCount() { - lock (WorkerLock) + lock (Lock) return Workers.Count; } public int GetPlannedWorkerCount() { - lock (WorkerLock) + lock (Lock) return PlannedWorkerCount; } public async Task Reset(CancellationToken cancellationToken = default) { while (true) { - ValueTask updateTask; - lock (WorkerLock) - updateTask = UpdateWorkerCount(MinWorkerCount - PlannedWorkerCount); - - await updateTask.ConfigureAwait(false); - lock (WorkerLock) { - if (Workers.Count == MinWorkerCount) { - lock (MetricsLock) { - ProcessedItemCount = 0; - MinQueueDurationInTicks = long.MaxValue; - } + var wp = WorkerPolicy; + await AddOrRemoveWorkers(wp.MinWorkerCount - GetPlannedWorkerCount(), true).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromSeconds(0.05), cancellationToken).ConfigureAwait(false); + lock (Lock) { + if (Workers.Count == wp.MinWorkerCount && PlannedWorkerCount == wp.MinWorkerCount) { + RecentReports.Clear(); + CooldownEndsAt = default; return; } } - await Task.Delay(TimeSpan.FromSeconds(0.05), cancellationToken).ConfigureAwait(false); } } // Protected methods - protected int UpdateMetrics(Item? item = null) + protected void ProcessWorkerReport(TimeSpan workerMinQueueTime) { - if (item != null && item.DequeuedAt == default) - return 0; // Something is off / the item was never processed - - var queueDuration = item != null - ? (item.DequeuedAt - item.QueuedAt).Ticks - : 0; - TimeSpan minQueueDuration; - lock (MetricsLock) { - ProcessedItemCount += 1; - MinQueueDurationInTicks = Math.Min(queueDuration, MinQueueDurationInTicks); + var minQueueTime = TimeSpan.MaxValue; + lock (Lock) { + RecentReports.PushHeadAndMoveTailIfFull(workerMinQueueTime); var now = CpuTimestamp.Now; - if (item != null && ProcessedItemCount < AdjustmentInterval) - return 0; - - minQueueDuration = TimeSpan.FromTicks(MinQueueDurationInTicks); - ProcessedItemCount = 0; - MinQueueDurationInTicks = long.MaxValue; - if (now - LastAdjustmentAt < AdjustmentPeriod) - return 0; + if (CooldownEndsAt > now) + return; - LastAdjustmentAt = now; + var recentReportCount = (PlannedWorkerCount >> 2).Clamp(1, RecentReports.Count); + for (var i = 0; i < recentReportCount; i++) + minQueueTime = TimeSpanExt.Min(minQueueTime, RecentReports[i]); } - - if (minQueueDuration > Add4WorkersAt) - return 4; - if (minQueueDuration > AddWorkerAt) - return 1; - if (minQueueDuration < Kill8WorkersAt) - return -8; - if (minQueueDuration < KillWorkerAt) - return -1; - return 0; + var delta = WorkerPolicy.GetWorkerCountDelta(minQueueTime); + _ = AddOrRemoveWorkers(delta); } - protected async ValueTask UpdateWorkerCount(int delta) + protected async ValueTask AddOrRemoveWorkers(int delta, bool ignoreCooldown = false) { if (delta == 0) return; - lock (WorkerLock) { + var wp = WorkerPolicy; + lock (Lock) { + var now = CpuTimestamp.Now; + if (CooldownEndsAt > now && !ignoreCooldown) + return; + + CooldownEndsAt = now + WorkerPolicy.Cooldown; var oldPlannedWorkerCount = PlannedWorkerCount; - PlannedWorkerCount = (oldPlannedWorkerCount + delta).Clamp(MinWorkerCount, MaxWorkerCount); - delta = PlannedWorkerCount - oldPlannedWorkerCount; + PlannedWorkerCount = (oldPlannedWorkerCount + delta).Clamp(wp.MinWorkerCount, wp.MaxWorkerCount); if (StopToken.IsCancellationRequested) { PlannedWorkerCount = 0; Queue.Writer.TryComplete(); return; } + + delta = PlannedWorkerCount - oldPlannedWorkerCount; } if (delta == 0) return; @@ -168,43 +153,65 @@ protected async ValueTask UpdateWorkerCount(int delta) } } else { // workerDelta > 0 -> add workers + int workerCount; using var flowSuppressor = ExecutionContextExt.SuppressFlow(); - lock (WorkerLock) { + lock (Lock) { WorkerCollectorTask ??= Task.Run(RunWorkerCollector, CancellationToken.None); for (; delta > 0; delta--) { var workerTask = Task.Run(RunWorker, CancellationToken.None); Workers.Add(workerTask); _ = workerTask.ContinueWith(static (task, state) => { var self = (BatchProcessor)state!; - lock (self.WorkerLock) + lock (self.Lock) self.Workers.Remove(task); }, this, TaskScheduler.Default); } + workerCount = Workers.Count; } + if (workerCount >= wp.MaxWorkerCount) + Log?.LogWarning("{BatchProcessor}: High worker count: {WorkerCount}", + GetType().GetName(), workerCount); } } protected virtual async Task RunWorkerCollector() { - var longCycle = WorkerCollectionPeriod; - var shortCycle = TimeSpan.FromSeconds(WorkerCollectionPeriod.TotalSeconds / 4); - var nextCycle = longCycle; - while (!StopToken.IsCancellationRequested) - try { - await Task.Delay(nextCycle, StopToken).ConfigureAwait(false); - var workerDelta = UpdateMetrics(); - await UpdateWorkerCount(workerDelta).ConfigureAwait(false); - nextCycle = workerDelta < 0 ? shortCycle : longCycle; - } - catch { - nextCycle = shortCycle; - } + while (!StopToken.IsCancellationRequested) { + var wp = WorkerPolicy; + var longCycle = wp.CollectorCycle; + var shortCycle = wp.Cooldown + TimeSpan.FromMilliseconds(50); + var nextCycle = longCycle; + while (!StopToken.IsCancellationRequested) + try { + await Task.Delay(nextCycle, StopToken).ConfigureAwait(false); + + // Measure queue time + var item = new Item(default!, StopToken) { IsMeasureOnlyItem = true }; + await Queue.Writer.WriteAsync(item).ConfigureAwait(false); + await item.ResultTask.ConfigureAwait(false); + var minQueueTime = item.DequeuedAt - item.QueuedAt; + + // Adjust worker count + var delta = WorkerPolicy.GetWorkerCountDelta(minQueueTime); + await AddOrRemoveWorkers(delta, true).ConfigureAwait(false); + + // Decide on how quickly to run the next cycle + nextCycle = delta < 0 ? shortCycle : longCycle; + if (!ReferenceEquals(WorkerPolicy, wp)) + break; + } + catch { + nextCycle = shortCycle; + } + } } protected virtual async Task RunWorker() { var reader = Queue.Reader; var batch = new List(BatchSize); + var minQueueTime = TimeSpan.MaxValue; + var reportCounter = 0; try { while (await reader.WaitToReadAsync().ConfigureAwait(false)) { while (reader.TryRead(out var item)) { @@ -213,6 +220,17 @@ protected virtual async Task RunWorker() return; } item.DequeuedAt = CpuTimestamp.Now; + if (item.IsMeasureOnlyItem) { + item.SetResult(default(TResult)!); + continue; + } + var queueTime = item.DequeuedAt - item.QueuedAt; + minQueueTime = TimeSpanExt.Min(queueTime, minQueueTime); + if (++reportCounter >= BatchSize) { + ProcessWorkerReport(minQueueTime); + reportCounter = 0; + minQueueTime = TimeSpan.MaxValue; + } batch.Add(item); if (batch.Count >= BatchSize) await ProcessBatch(batch).ConfigureAwait(false); @@ -220,6 +238,9 @@ protected virtual async Task RunWorker() await ProcessBatch(batch).ConfigureAwait(false); } } + catch (Exception e) { + Log?.LogError(e, "{BatchProcessor}: Worker failed", GetType().GetName()); + } finally { await ProcessBatch(batch).ConfigureAwait(false); } @@ -276,9 +297,7 @@ private async Task CompleteProcessBatchAsync(List batch, Task resultTask) private Task CompleteProcessBatch(List batch, Exception? error = null) { - var completedAt = CpuTimestamp.Now; foreach (var item in batch) { - item.CompletedAt = completedAt; if (error == null) item.SetError(Errors.UnprocessedBatchItem()); else if (error is OperationCanceledException) @@ -294,19 +313,19 @@ private Task CompleteProcessBatch(List batch, Exception? error = null) public class Item(T input, TaskCompletionSource resultSource, CancellationToken cancellationToken) { - private static readonly TaskCompletionSource KillSource + private static readonly TaskCompletionSource WorkerKillSource = TaskCompletionSourceExt.New() - .WithException(Errors.InternalError("Something is off: you should never see the KillItem in batches.")); + .WithException(Errors.InternalError("Something is off: you should never see the WorkerKiller item in batches.")); - public static readonly Item WorkerKiller = new(default!, KillSource, default); + public static readonly Item WorkerKiller = new(default!, WorkerKillSource, default); public readonly T Input = input; public readonly TaskCompletionSource ResultSource = resultSource; public readonly CancellationToken CancellationToken = cancellationToken; public readonly CpuTimestamp QueuedAt = CpuTimestamp.Now; public CpuTimestamp DequeuedAt; - public CpuTimestamp CompletedAt; public Task ResultTask => ResultSource.Task; + public bool IsMeasureOnlyItem; public Item(T input, CancellationToken cancellationToken) : this(input, TaskCompletionSourceExt.New(), cancellationToken) diff --git a/src/Stl/Async/BatchProcessorWorkerPolicy.cs b/src/Stl/Async/BatchProcessorWorkerPolicy.cs new file mode 100644 index 000000000..9dcc1d50a --- /dev/null +++ b/src/Stl/Async/BatchProcessorWorkerPolicy.cs @@ -0,0 +1,46 @@ +using Stl.OS; + +namespace Stl.Async; + +public interface IBatchProcessorWorkerPolicy +{ + int MinWorkerCount { get; } + int MaxWorkerCount { get; } + + TimeSpan Cooldown { get; } + TimeSpan CollectorCycle { get; } + + int GetWorkerCountDelta(TimeSpan minQueueTime); +} + +public record BatchProcessorWorkerPolicy : IBatchProcessorWorkerPolicy +{ + public static IBatchProcessorWorkerPolicy Default { get; set; } = new BatchProcessorWorkerPolicy(); + + public int MinWorkerCount { get; init; } = 1; + public int MaxWorkerCount { get; init; } = HardwareInfo.GetProcessorCountFactor(2); + + public TimeSpan KillWorkerAt { get; init; } = TimeSpan.FromMilliseconds(1); + public TimeSpan Kill8WorkersAt { get; init; } = TimeSpan.FromMilliseconds(0.1); + public TimeSpan AddWorkerAt { get; init; } = TimeSpan.FromMilliseconds(20); + public TimeSpan Add4WorkersAt { get; init; } = TimeSpan.FromMilliseconds(100); + public TimeSpan Add8WorkersAt { get; init; } = TimeSpan.FromMilliseconds(500); + + public TimeSpan Cooldown { get; init; } = TimeSpan.FromMilliseconds(100); + public TimeSpan CollectorCycle { get; set; } = TimeSpan.FromSeconds(5); + + public virtual int GetWorkerCountDelta(TimeSpan minQueueTime) + { + if (minQueueTime > Add8WorkersAt) + return 8; + if (minQueueTime > Add4WorkersAt) + return 4; + if (minQueueTime > AddWorkerAt) + return 1; + if (minQueueTime < Kill8WorkersAt) + return -8; + if (minQueueTime < KillWorkerAt) + return -1; + return 0; + } +} diff --git a/src/Stl/Collections/RingBuffer.cs b/src/Stl/Collections/RingBuffer.cs index 1961f9038..da281f205 100644 --- a/src/Stl/Collections/RingBuffer.cs +++ b/src/Stl/Collections/RingBuffer.cs @@ -125,6 +125,14 @@ public void PushHead(T head) _buffer[_start] = head; } + public void PushHeadAndMoveTailIfFull(T head) + { + if (IsFull) + _end = (_end - 1) & Capacity; + _start = (_start - 1) & Capacity; + _buffer[_start] = head; + } + public void PushTail(T tail) { AssertNotFull(); @@ -132,6 +140,14 @@ public void PushTail(T tail) _end = (_end + 1) & Capacity; } + public void PushTailAndMoveHeadIfFull(T tail) + { + if (IsFull) + _start = (_start + 1) & Capacity; + _buffer[_end] = tail; + _end = (_end + 1) & Capacity; + } + public void Clear() => _end = _start = 0; diff --git a/tests/Stl.Tests/Async/BatchProcessorTest.cs b/tests/Stl.Tests/Async/BatchProcessorTest.cs index 6bae44581..b056ac30c 100644 --- a/tests/Stl.Tests/Async/BatchProcessorTest.cs +++ b/tests/Stl.Tests/Async/BatchProcessorTest.cs @@ -3,18 +3,18 @@ namespace Stl.Tests.Async; [Collection(nameof(TimeSensitiveTests)), Trait("Category", nameof(TimeSensitiveTests))] -public class BatchProcessorTest : TestBase +public class BatchProcessorTest(ITestOutputHelper @out) : TestBase(@out) { - public BatchProcessorTest(ITestOutputHelper @out) : base(@out) { } - [Fact] public async Task BasicTest() { var batchIndex = 0; await using var processor = new BatchProcessor() { - MinWorkerCount = 1, - MaxWorkerCount = 3, BatchSize = 3, + WorkerPolicy = new BatchProcessorWorkerPolicy { + MinWorkerCount = 1, + MaxWorkerCount = 3, + }, Implementation = async (batch, cancellationToken) => { var bi = Interlocked.Increment(ref batchIndex); await Task.Delay(100).ConfigureAwait(false); @@ -47,8 +47,8 @@ async Task Reset() { tasks = Enumerable.Range(0, 6).Select(i => processor.Process(i)).ToArray(); await Task.WhenAll(tasks); tasks.Count(t => t.Result.BatchIndex == 1).Should().Be(1); - tasks.Count(t => t.Result.BatchIndex == 2).Should().Be(3); - tasks.Count(t => t.Result.BatchIndex == 3).Should().Be(2); + tasks.Count(t => t.Result.BatchIndex == 2).Should().BeOneOf(2, 3); + tasks.Count(t => t.Result.BatchIndex == 3).Should().BeOneOf(2, 3); tasks.Select(t => t.Result.Value).Should().BeEquivalentTo(Enumerable.Range(0, 6)); await Reset(); @@ -95,34 +95,58 @@ public async Task WorkerRampUpTest() if (TestRunnerInfo.IsBuildAgent()) return; + var batchDelay = 100; + + var services = CreateLoggingServices(@out); await using var processor = new BatchProcessor() { - MinWorkerCount = 1, - MaxWorkerCount = 100, BatchSize = 10, - WorkerCollectionPeriod = TimeSpan.FromSeconds(1), + WorkerPolicy = new BatchProcessorWorkerPolicy() { + MinWorkerCount = 1, + MaxWorkerCount = 100, + CollectorCycle = TimeSpan.FromSeconds(1), + }, Implementation = async (batch, cancellationToken) => { - await Task.Delay(100, cancellationToken).ConfigureAwait(false); + await Task.Delay(batchDelay, cancellationToken).ConfigureAwait(false); foreach (var item in batch) item.SetResult(item.Input); - } + }, + Log = services.LogFor>(), }; - var tasks = new Task[10_000]; - for (var i = 0; i < tasks.Length; i++) - tasks[i] = processor.Process(i); - while (true) { - await Delay(0.5); - Out.WriteLine($"WorkerCount: {processor.GetWorkerCount()}"); - if (processor.GetWorkerCount() > 60) - break; - } - while (true) { - await Delay(0.5); - Out.WriteLine($"WorkerCount: {processor.GetWorkerCount()}"); - if (processor.GetWorkerCount() == 1) - break; + processor.GetWorkerCount().Should().Be(0); + processor.GetPlannedWorkerCount().Should().Be(0); + await processor.Process(0); + processor.GetWorkerCount().Should().Be(1); + processor.GetPlannedWorkerCount().Should().Be(1); + + batchDelay = 10; + await Test(1000, 5); + + batchDelay = 100; + await Test(1000, 10); + await Test(10_000, 60); + + async Task Test(int taskCount, int minExpectedWorkerCount) { + Out.WriteLine($"Task count: {taskCount}, batch delay: {batchDelay}"); + var tasks = new Task[taskCount]; + for (var i = 0; i < tasks.Length; i++) + tasks[i] = processor.Process(i); + + while (true) { + await Delay(0.5); + Out.WriteLine($"WorkerCount: {processor.GetWorkerCount()}"); + if (processor.GetWorkerCount() >= minExpectedWorkerCount) + break; + } + while (true) { + await Delay(0.5); + Out.WriteLine($"WorkerCount: {processor.GetWorkerCount()}"); + if (processor.GetWorkerCount() == 1) + break; + } + await Task.WhenAll(tasks); + tasks.Where((t, i) => t.Result != i).Count().Should().Be(0); + Out.WriteLine(""); } - await Task.WhenAll(tasks); - tasks.Where((t, i) => t.Result != i).Count().Should().Be(0); } } diff --git a/tests/Stl.Tests/CommandR/CommandRTestBase.cs b/tests/Stl.Tests/CommandR/CommandRTestBase.cs index dead7d8e0..d089cdc3a 100644 --- a/tests/Stl.Tests/CommandR/CommandRTestBase.cs +++ b/tests/Stl.Tests/CommandR/CommandRTestBase.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; using Stl.Fusion.EntityFramework; using Stl.IO; -using Stl.Testing.Output; using Stl.Tests.CommandR.Services; +using Xunit.DependencyInjection; using Xunit.DependencyInjection.Logging; namespace Stl.Tests.CommandR; @@ -49,7 +49,7 @@ bool LogFilter(string? category, LogLevel level) logging.AddProvider( #pragma warning disable CS0618 new XunitTestOutputLoggerProvider( - new TestOutputHelperAccessor(Out), + new TestOutputHelperAccessor() { Output = Out }, LogFilter)); #pragma warning restore CS0618 }); diff --git a/tests/Stl.Tests/Plugins/PluginTest.cs b/tests/Stl.Tests/Plugins/PluginTest.cs index 3db82bcc4..522b5b840 100644 --- a/tests/Stl.Tests/Plugins/PluginTest.cs +++ b/tests/Stl.Tests/Plugins/PluginTest.cs @@ -1,7 +1,7 @@ using Stl.Caching; using Stl.Plugins; using Stl.Reflection; -using Stl.Testing.Output; +using Xunit.DependencyInjection; using Xunit.DependencyInjection.Logging; namespace Stl.Tests.Plugins; @@ -80,7 +80,7 @@ private PluginHostBuilder CreateHostBuilder(bool mustClearCache = false) logging.AddProvider( #pragma warning disable CS0618 new XunitTestOutputLoggerProvider( - new TestOutputHelperAccessor(Out), + new TestOutputHelperAccessor() { Output = Out }, (_, level) => level >= LogLevel.Debug)); #pragma warning restore CS0618 }); diff --git a/tests/Stl.Tests/Rpc/RpcLocalTestBase.cs b/tests/Stl.Tests/Rpc/RpcLocalTestBase.cs index 6bfff258a..252e57a5c 100644 --- a/tests/Stl.Tests/Rpc/RpcLocalTestBase.cs +++ b/tests/Stl.Tests/Rpc/RpcLocalTestBase.cs @@ -1,6 +1,6 @@ using Stl.Rpc; using Stl.Rpc.Testing; -using Stl.Testing.Output; +using Xunit.DependencyInjection; using Xunit.DependencyInjection.Logging; namespace Stl.Tests.Rpc; @@ -35,7 +35,7 @@ protected virtual void ConfigureServices(ServiceCollection services) logging.AddProvider( #pragma warning disable CS0618 new XunitTestOutputLoggerProvider( - new TestOutputHelperAccessor(Out), + new TestOutputHelperAccessor() { Output = Out }, (_, level) => level >= LogLevel.Debug)); #pragma warning restore CS0618 }); diff --git a/tests/Stl.Tests/RpcTestBase.cs b/tests/Stl.Tests/RpcTestBase.cs index 909edf576..58fc3894d 100644 --- a/tests/Stl.Tests/RpcTestBase.cs +++ b/tests/Stl.Tests/RpcTestBase.cs @@ -4,8 +4,8 @@ using Stl.Rpc; using Stl.Rpc.Clients; using Stl.Testing.Collections; -using Stl.Testing.Output; using Stl.Time.Testing; +using Xunit.DependencyInjection; using Xunit.DependencyInjection.Logging; namespace Stl.Tests; @@ -103,7 +103,7 @@ bool LogFilter(string? category, LogLevel level) logging.AddProvider( #pragma warning disable CS0618 new XunitTestOutputLoggerProvider( - new TestOutputHelperAccessor(Out), + new TestOutputHelperAccessor() { Output = Out }, LogFilter)); #pragma warning restore CS0618 }); diff --git a/tests/Stl.Tests/TestHelpers.cs b/tests/Stl.Tests/TestHelpers.cs index e6aec851d..20e045dbd 100644 --- a/tests/Stl.Tests/TestHelpers.cs +++ b/tests/Stl.Tests/TestHelpers.cs @@ -1,5 +1,7 @@ using Stl.Generators; using Stl.Rpc; +using Xunit.DependencyInjection; +using Xunit.DependencyInjection.Logging; namespace Stl.Tests; @@ -22,6 +24,24 @@ public static void GCCollect() } } + public static IServiceProvider CreateLoggingServices(ITestOutputHelper @out) + { + var services = new ServiceCollection(); + services.AddLogging(logging => { + logging.ClearProviders(); + logging.SetMinimumLevel(LogLevel.Debug); + logging.AddDebug(); + logging.Services.AddSingleton(_ => { +#pragma warning disable CS0618 + return new XunitTestOutputLoggerProvider( + new TestOutputHelperAccessor() { Output = @out }, + (_, level) => level >= LogLevel.Debug); +#pragma warning restore CS0618 + }); + }); + return services.BuildServiceProvider(); + } + // Rpc public static Task AssertNoCalls(RpcPeer peer)