From 03631cd0c847d6df916fcbd81e43be244d6ce880 Mon Sep 17 00:00:00 2001 From: max Date: Wed, 10 Sep 2025 01:46:07 +0200 Subject: [PATCH] [CHANGE] Service extended and events --- .../Components/Layout/ApplicationLayout.razor | 2 +- .../Components/Layout/BaseLayout.razor | 2 +- Manager.App/Components/Pages/Services.razor | 76 +++++++++++++++++-- .../Components/Pages/Services.razor.cs | 53 ++++++++++++- .../Extensions/AsyncEnumerableExtensions.cs | 48 ++++++++++++ Manager.App/Services/CircularBuffer.cs | 21 +++-- .../Services/ExtendedBackgroundService.cs | 12 +-- Manager.App/Services/System/ClientService.cs | 10 ++- Manager.App/appsettings.Development.json | 4 +- 9 files changed, 202 insertions(+), 26 deletions(-) create mode 100644 Manager.App/Extensions/AsyncEnumerableExtensions.cs diff --git a/Manager.App/Components/Layout/ApplicationLayout.razor b/Manager.App/Components/Layout/ApplicationLayout.razor index d4fb985..37590f8 100644 --- a/Manager.App/Components/Layout/ApplicationLayout.razor +++ b/Manager.App/Components/Layout/ApplicationLayout.razor @@ -16,7 +16,7 @@ } -
+
@Body
\ No newline at end of file diff --git a/Manager.App/Components/Layout/BaseLayout.razor b/Manager.App/Components/Layout/BaseLayout.razor index 04778dd..f1dca2b 100644 --- a/Manager.App/Components/Layout/BaseLayout.razor +++ b/Manager.App/Components/Layout/BaseLayout.razor @@ -7,7 +7,7 @@ - + @Body diff --git a/Manager.App/Components/Pages/Services.razor b/Manager.App/Components/Pages/Services.razor index 574a7de..b04c4b2 100644 --- a/Manager.App/Components/Pages/Services.razor +++ b/Manager.App/Components/Pages/Services.razor @@ -1,26 +1,90 @@ @page "/Services" @using Manager.App.Services.System - +@implements IDisposable @inject BackgroundServiceManager ServiceManager Services - + Services - + + - + + + + Pause + + Resume + + + + - + - \ No newline at end of file + + + + + Service events + @($"{_serviceEvents.Count}/{VisibleEventCapacity} events") + +
+ @foreach (var serviceEvent in _serviceEvents) + { +
+ @serviceEvent.Date + | + @serviceEvent.Severity + | + @serviceEvent.Source + - + @serviceEvent.Message +
+ } +
+
+ + \ No newline at end of file diff --git a/Manager.App/Components/Pages/Services.razor.cs b/Manager.App/Components/Pages/Services.razor.cs index a28b9bb..a0bc3a1 100644 --- a/Manager.App/Components/Pages/Services.razor.cs +++ b/Manager.App/Components/Pages/Services.razor.cs @@ -1,16 +1,67 @@ +using DotBased.Logging; +using Manager.App.Extensions; using Manager.App.Services; using Microsoft.AspNetCore.Components; -using MudBlazor; namespace Manager.App.Components.Pages; public partial class Services : ComponentBase { + private const int VisibleEventCapacity = 500; private string _searchText = ""; private List _backgroundServices = []; + + private List _serviceEvents = []; + private CancellationTokenSource _cts = new(); protected override void OnInitialized() { _backgroundServices = ServiceManager.GetServices(); + _ = Task.Run(() => ReadEventStreamsAsync(_cts.Token)); + } + + private Func QuickFilter + => x => string.IsNullOrWhiteSpace(_searchText) || $"{x.Name} {x.Description} {x.State} {x.ExecuteInterval}".Contains(_searchText); + + private async Task ReadEventStreamsAsync(CancellationToken token) + { + if (_backgroundServices.Count == 0) + { + return; + } + + var totalToGet = VisibleEventCapacity / _backgroundServices.Count; + _serviceEvents.AddRange(_backgroundServices.SelectMany(x => x.ProgressEvents.Items.TakeLast(totalToGet))); + + var asyncEnumerators = _backgroundServices.Select(x => x.ProgressEvents.GetStreamAsync()); + await foreach (var serviceEvent in AsyncEnumerableExtensions.Merge(asyncEnumerators, token)) + { + if (!_serviceEvents.Contains(serviceEvent)) + { + _serviceEvents.Add(serviceEvent); + } + + if (_serviceEvents.Count > VisibleEventCapacity) + { + _serviceEvents.RemoveAt(0); + } + + await InvokeAsync(StateHasChanged); + } + } + + private string GetLogClass(ServiceEvent serviceEvent) => + serviceEvent.Severity switch + { + LogSeverity.Info => "log-info", + LogSeverity.Warning => "log-warning", + LogSeverity.Error => "log-error", + _ => "log-info" + }; + + public void Dispose() + { + _cts.Cancel(); + _cts.Dispose(); } } \ No newline at end of file diff --git a/Manager.App/Extensions/AsyncEnumerableExtensions.cs b/Manager.App/Extensions/AsyncEnumerableExtensions.cs new file mode 100644 index 0000000..d8bac98 --- /dev/null +++ b/Manager.App/Extensions/AsyncEnumerableExtensions.cs @@ -0,0 +1,48 @@ +using System.Threading.Channels; + +namespace Manager.App.Extensions; + +public static class AsyncEnumerableExtensions +{ + public static async IAsyncEnumerable Merge(IEnumerable> sources, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var channel = Channel.CreateUnbounded( new UnboundedChannelOptions { SingleReader = true, SingleWriter = false }); + + var writerTasks = sources.Select(source => Task.Run(async () => + { + try + { + await foreach (var item in source.WithCancellation(cancellationToken)) + { + await channel.Writer.WriteAsync(item, cancellationToken); + } + } + catch (OperationCanceledException) + { + + } + catch (Exception ex) + { + channel.Writer.TryComplete(ex); + } + }, cancellationToken)).ToArray(); + + _ = Task.Run(async () => + { + try + { + await Task.WhenAll(writerTasks); + channel.Writer.TryComplete(); + } + catch + { + channel.Writer.TryComplete(); + } + }, cancellationToken); + + await foreach (var item in channel.Reader.ReadAllAsync(cancellationToken)) + { + yield return item; + } + } +} \ No newline at end of file diff --git a/Manager.App/Services/CircularBuffer.cs b/Manager.App/Services/CircularBuffer.cs index fddb645..fb7afc7 100644 --- a/Manager.App/Services/CircularBuffer.cs +++ b/Manager.App/Services/CircularBuffer.cs @@ -6,6 +6,7 @@ public class CircularBuffer { private readonly T[] _buffer; private readonly Channel _channel; + private readonly object _lock = new(); public int Capacity { get; } public int Head { get; private set; } @@ -30,14 +31,17 @@ public class CircularBuffer public void Add(T item) { - _buffer[Head] = item; - Head = (Head + 1) % _buffer.Length; - - if (Count < _buffer.Length) + lock (_lock) { - Count++; + _buffer[Head] = item; + Head = (Head + 1) % _buffer.Length; + + if (Count < _buffer.Length) + { + Count++; + } } - + _channel.Writer.TryWrite(item); } @@ -47,7 +51,10 @@ public class CircularBuffer { for (var i = 0; i < Count; i++) { - yield return _buffer[(Head - Count + i + _buffer.Length) % _buffer.Length]; + lock (_lock) + { + yield return _buffer[(Head - Count + i + _buffer.Length) % _buffer.Length]; + } } } } diff --git a/Manager.App/Services/ExtendedBackgroundService.cs b/Manager.App/Services/ExtendedBackgroundService.cs index a2e3721..7ebba01 100644 --- a/Manager.App/Services/ExtendedBackgroundService.cs +++ b/Manager.App/Services/ExtendedBackgroundService.cs @@ -9,16 +9,18 @@ public abstract class ExtendedBackgroundService : BackgroundService private TaskCompletionSource _resumeSignal = new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly ILogger _logger; public ServiceState State { get; private set; } = ServiceState.Stopped; - public CircularBuffer ProgressLog { get; } = new(500); + public CircularBuffer ProgressEvents { get; } = new(500); public string Name { get; } + public string Description { get; set; } public TimeSpan ExecuteInterval { get; set; } - public ExtendedBackgroundService(string name, ILogger logger, BackgroundServiceManager manager, TimeSpan? executeInterval = null) + public ExtendedBackgroundService(string name, string description, ILogger logger, BackgroundServiceManager manager, TimeSpan? executeInterval = null) { Name = name; + Description = description; _logger = logger; manager.RegisterService(this); - ExecuteInterval = executeInterval ?? TimeSpan.Zero; + ExecuteInterval = executeInterval ?? TimeSpan.FromMinutes(1); } protected sealed override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -58,7 +60,7 @@ public abstract class ExtendedBackgroundService : BackgroundService } } - protected void LogProgress(string message, LogSeverity severity = LogSeverity.Info) => ProgressLog.Add(new ServiceProgress(message, DateTime.UtcNow, severity)); + protected void LogEvent(string message, LogSeverity severity = LogSeverity.Info) => ProgressEvents.Add(new ServiceEvent(Name, message, DateTime.UtcNow, severity)); public void Pause() { @@ -101,4 +103,4 @@ public enum ServiceState Paused } -public record ServiceProgress(string Message, DateTime StartTime, LogSeverity Severity); \ No newline at end of file +public record ServiceEvent(string Source, string Message, DateTime Date, LogSeverity Severity); \ No newline at end of file diff --git a/Manager.App/Services/System/ClientService.cs b/Manager.App/Services/System/ClientService.cs index f1c1878..ed6dbb7 100644 --- a/Manager.App/Services/System/ClientService.cs +++ b/Manager.App/Services/System/ClientService.cs @@ -1,4 +1,5 @@ using System.Net; +using DotBased.Logging; using DotBased.Monads; using Manager.App.Models.Library; using Manager.Data.Entities.LibraryContext; @@ -6,7 +7,8 @@ using Manager.YouTube; namespace Manager.App.Services.System; -public class ClientService(IServiceScopeFactory scopeFactory, ILogger logger, BackgroundServiceManager serviceManager) : ExtendedBackgroundService("ClientService", logger, serviceManager) +public class ClientService(IServiceScopeFactory scopeFactory, ILogger logger, BackgroundServiceManager serviceManager) + : ExtendedBackgroundService("ClientService", "Managing YouTube clients", logger, serviceManager, TimeSpan.FromMilliseconds(100)) { private readonly List _clients = []; private CancellationToken _cancellationToken; @@ -18,13 +20,15 @@ public class ClientService(IServiceScopeFactory scopeFactory, ILogger(); - LogProgress("Initializing service..."); + LogEvent("Initializing service..."); Pause(); } protected override async Task ExecuteServiceAsync(CancellationToken stoppingToken) { - + LogEvent("Sending event..."); + LogEvent("Sending warning event...", LogSeverity.Warning); + LogEvent("Sending error event...", LogSeverity.Error); } private void CancellationRequested() diff --git a/Manager.App/appsettings.Development.json b/Manager.App/appsettings.Development.json index abae8bb..91be6ab 100644 --- a/Manager.App/appsettings.Development.json +++ b/Manager.App/appsettings.Development.json @@ -3,11 +3,11 @@ "Logging": { "Severity": "Debug", "SeverityFilters":{ - "Microsoft": "Debug", + "Microsoft": "Info", "Microsoft.Hosting.Lifetime": "Debug", "Microsoft.AspNetCore": "Warning", "Microsoft.AspNetCore.Authentication": "Debug", - "MudBlazor": "Debug" + "MudBlazor": "Info" } } },