From 7ae47b2f2f45c5a01fd4d2181ffb5ee44bfb30d3 Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Tue, 4 Jul 2023 13:51:47 +0200 Subject: [PATCH 01/23] Add AsyncService class. Same as old AsyncWorker for now. --- src/Louis/PublicAPI.Unshipped.txt | 22 ++ src/Louis/Threading/AsyncService.cs | 366 +++++++++++++++++++++++ src/Louis/Threading/AsyncServiceState.cs | 40 +++ 3 files changed, 428 insertions(+) create mode 100644 src/Louis/Threading/AsyncService.cs create mode 100644 src/Louis/Threading/AsyncServiceState.cs diff --git a/src/Louis/PublicAPI.Unshipped.txt b/src/Louis/PublicAPI.Unshipped.txt index 336f9cf..a7d2500 100644 --- a/src/Louis/PublicAPI.Unshipped.txt +++ b/src/Louis/PublicAPI.Unshipped.txt @@ -1,4 +1,7 @@ #nullable enable +abstract Louis.Threading.AsyncService.OnStartServiceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +abstract Louis.Threading.AsyncService.OnStopServiceAsync() -> System.Threading.Tasks.ValueTask +abstract Louis.Threading.AsyncService.RunServiceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! const Louis.Diagnostics.ExceptionHelper.NullText = "" -> string! const Louis.Diagnostics.ExceptionHelper.ToStringEmptyText = "" -> string! const Louis.Diagnostics.ExceptionHelper.ToStringNullText = "" -> string! @@ -34,6 +37,24 @@ Louis.Text.StringLiteralKind.Quoted = 0 -> Louis.Text.StringLiteralKind Louis.Text.StringLiteralKind.Verbatim = 1 -> Louis.Text.StringLiteralKind Louis.Text.UnicodeCharacterUtility Louis.Text.Utf8Utility +Louis.Threading.AsyncService +Louis.Threading.AsyncService.AsyncService() -> void +Louis.Threading.AsyncService.Dispose() -> void +Louis.Threading.AsyncService.DisposeAsync() -> System.Threading.Tasks.ValueTask +Louis.Threading.AsyncService.RunAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Louis.Threading.AsyncService.StartAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Louis.Threading.AsyncService.State.get -> Louis.Threading.AsyncServiceState +Louis.Threading.AsyncService.StopAsync() -> System.Threading.Tasks.Task! +Louis.Threading.AsyncService.TryStop() -> bool +Louis.Threading.AsyncService.WaitUntilStartedAsync() -> System.Threading.Tasks.Task! +Louis.Threading.AsyncService.WaitUntilStoppedAsync() -> System.Threading.Tasks.Task! +Louis.Threading.AsyncServiceState +Louis.Threading.AsyncServiceState.Created = 0 -> Louis.Threading.AsyncServiceState +Louis.Threading.AsyncServiceState.Disposed = 5 -> Louis.Threading.AsyncServiceState +Louis.Threading.AsyncServiceState.Running = 2 -> Louis.Threading.AsyncServiceState +Louis.Threading.AsyncServiceState.Starting = 1 -> Louis.Threading.AsyncServiceState +Louis.Threading.AsyncServiceState.Stopped = 4 -> Louis.Threading.AsyncServiceState +Louis.Threading.AsyncServiceState.Stopping = 3 -> Louis.Threading.AsyncServiceState Louis.Threading.InterlockedFlag Louis.Threading.InterlockedFlag.CheckAndSet(bool value) -> bool Louis.Threading.InterlockedFlag.Equals(Louis.Threading.InterlockedFlag other) -> bool @@ -167,3 +188,4 @@ static Louis.Threading.InterlockedFlag.operator ==(Louis.Threading.InterlockedFl static Louis.Threading.ValueTaskUtility.WhenAll(params System.Threading.Tasks.ValueTask[]! valueTasks) -> System.Threading.Tasks.ValueTask static Louis.Threading.ValueTaskUtility.WhenAll(System.Collections.Generic.IEnumerable! valueTasks) -> System.Threading.Tasks.ValueTask static readonly Louis.Text.Utf8Utility.Utf8NoBomEncoding -> System.Text.Encoding! +virtual Louis.Threading.AsyncService.OnDisposeServiceAsync() -> System.Threading.Tasks.ValueTask diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs new file mode 100644 index 0000000..a5e36f5 --- /dev/null +++ b/src/Louis/Threading/AsyncService.cs @@ -0,0 +1,366 @@ +// Copyright (c) Tenacom and contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Diagnostics; +using Louis.Diagnostics; + +namespace Louis.Threading; + +/// +/// Base class for long-running services. +/// +/// +/// This class differs from the BackgroundService +/// class found in Microsoft.Extensions.Hosting because it gives you more control over your service: +/// +/// you can either start your service in background by calling , +/// or run it as a task by calling ; +/// you can, optionally, override the and methods +/// to separate setup and teardown from the core of your service; +/// you can use the to know, at any time, if your service has been started, has finished starting, +/// is stopping, or has finished stopping; +/// you can synchronize other tasks with your service by calling +/// and ; +/// you can stop your service and let it complete in the background by calling , +/// or stop and wait for completion by calling . +/// +/// +public abstract class AsyncService : IAsyncDisposable, IDisposable +{ + private readonly CancellationTokenSource _stoppedTokenSource = new(); + private readonly TaskCompletionSource _startedCompletionSource = new(); + private readonly TaskCompletionSource _stoppedCompletionSource = new(); + private readonly object _stateSyncRoot = new(); + + private InterlockedFlag _disposed; + private AsyncServiceState _state = AsyncServiceState.Created; + + /// + /// Initializes a new instance of the class. + /// + protected AsyncService() + { + } + + /// + /// Gets the state of the service. + /// + /// + public AsyncServiceState State + { + get + { + lock (_stateSyncRoot) + { + return _state; + } + } + private set + { + lock (_stateSyncRoot) + { + _state = value; + } + } + } + + /// + /// Asynchronously run an asynchronous service. + /// + /// A used to stop the service. + /// + /// A that will complete when the service has stopped. + /// + /// + /// The service has already been started, either by calling or . + /// + /// + /// You may call one of and at most once. + /// + public Task RunAsync(CancellationToken cancellationToken) => RunAsyncCore(false, cancellationToken); + + /// + /// Asynchronously start an asynchronous service, then return while it continues to run in the background. + /// + /// A used to stop the service. + /// + /// A Task<bool> that will complete as soon as the service has started + /// (in which case the result will be ), or it could not start, either because of an exception, + /// or the cancellation of (in which case the result will be ). + /// + /// + /// The service has already been started, either by calling or . + /// + /// + /// You may call one of and at most once. + /// If your program needs to know the exact reason why a service stops or fails to start, + /// do not use this method; call instead. + /// + public Task StartAsync(CancellationToken cancellationToken) + { + _ = RunAsyncCore(true, cancellationToken); + return WaitUntilStartedAsync(); + } + + /// + /// Asynchronously stops an asynchronous service. + /// + /// + /// A that will complete as soon as the service has finished stopping. + /// If the service has already stopped, the returned task is already completed. + /// + /// + /// The service has not been started yet. + /// + public Task StopAsync() => TryStop() ? WaitUntilStoppedAsync() : Task.CompletedTask; + + /// + /// Tries to stop an asynchronous service. + /// + /// + /// if the service has been stopped; + /// if it had already stopped. + /// + /// + /// The service has not been started yet. + /// + public bool TryStop() => TryStopCore(out var exception) || (exception is null ? false : throw exception); + + /// + /// Asynchronously wait until an asynchronous service has started. + /// + /// + /// A Task<bool> that will complete as soon as the service has started + /// (in which case the result will be ), or it could not start + /// (in which case the result will be ). + /// + /// + /// If neither nor have been called, + /// the returned task will not complete until one of them is called. + /// If the service has already finished starting, the returned task is already completed. + /// + public Task WaitUntilStartedAsync() => _startedCompletionSource.Task; + + /// + /// Asynchronously wait until an asynchronous service has stopped. + /// + /// + /// A that will complete as soon as the service has started and then stopped, + /// or until it has failed to start. + /// + /// If neither nor have been called, + /// the returned task will not complete until one of them is called. + /// If the service has already stopped, the returned task is already completed. + /// + public Task WaitUntilStoppedAsync() => _stoppedCompletionSource.Task; + + /// + /// + /// This method stops the service and waits for completion if it has been started, + /// then calls before releasing resources + /// held by this class. + /// + public async ValueTask DisposeAsync() + { + AsyncServiceState previousState; + lock (_stateSyncRoot) + { + if (!_disposed.TrySet()) + { + return; + } + + previousState = _state; + if (previousState < AsyncServiceState.Starting) + { + _state = AsyncServiceState.Disposed; + } + } + + if (previousState < AsyncServiceState.Starting) + { + _ = _startedCompletionSource.TrySetResult(false); + _ = _stoppedCompletionSource.TrySetResult(true); + } + else + { + if (TryStopCore(out var exception) && exception is null) + { + await WaitUntilStoppedAsync().ConfigureAwait(false); + } + + State = AsyncServiceState.Disposed; + } + + await OnDisposeServiceAsync().ConfigureAwait(false); + _stoppedTokenSource.Dispose(); + } + + /// + /// + /// This method exists for compatibility with older libraries (e.g. DI containers) lacking support for + /// . It just calls and waits for it synchronously. + /// If possible, you should use instead of this method. + /// +#pragma warning disable CA2012 // Use ValueTasks correctly - This is correct usage because we consume the ValueTask only once. + public void Dispose() => DisposeAsync().ConfigureAwait(false).GetAwaiter().GetResult(); +#pragma warning restore CA2012 // Use ValueTasks correctly + + /// + /// Asynchronously releases managed resources owned by this instance. + /// Note that an instance of a class derived from + /// cannot directly own unmanaged resources. + /// + /// A that represents the ongoing operation. + protected virtual ValueTask OnDisposeServiceAsync() => default; + + /// + /// When overridden in a derived class, performs asynchronous operations + /// related to starting the service. + /// + /// A used to stop the operation. + /// A that represents the ongoing operation. + protected abstract ValueTask OnStartServiceAsync(CancellationToken cancellationToken); + + /// + /// When overridden in a derived class, performs asynchronous operations + /// related to stopping the service. + /// + /// A that represents the ongoing operation. + /// + /// This method is called if and only if the returned by + /// completes successfully, even if the service is prevented from starting afterwards + /// (for example if the passed to + /// is canceled after checks it for the last time). + /// + protected abstract ValueTask OnStopServiceAsync(); + + /// + /// When overridden in a derived class, performs the actual operations the service is meant to carry out. + /// + /// A used to stop the service. + /// A representing the ongoing operation. + protected abstract Task RunServiceAsync(CancellationToken cancellationToken); + + [DoesNotReturn] + private static void ThrowOnObjectDisposed(string message) + => throw new ObjectDisposedException(message); + + [DoesNotReturn] + private static void ThrowMultipleExceptions(params Exception[] innerExceptions) + => throw new AggregateException(innerExceptions); + + private bool TryStopCore(out Exception? exception) + { + lock (_stateSyncRoot) + { + switch (_state) + { + case < AsyncServiceState.Starting: + exception = new InvalidOperationException("The service has not been started yet."); + return false; + case > AsyncServiceState.Running: + exception = null; + return false; + } + + _state = AsyncServiceState.Stopping; + } + + _stoppedTokenSource.Cancel(); + exception = null; + return true; + } + + private async Task RunAsyncCore(bool runInBackground, CancellationToken cancellationToken) + { + lock (_stateSyncRoot) + { + switch (_state) + { + case AsyncServiceState.Created: + _state = AsyncServiceState.Starting; + break; + case AsyncServiceState.Disposed: + ThrowOnObjectDisposed("Trying to run a disposed async service."); + break; + default: + ThrowHelper.ThrowInvalidOperationException("An async service cannot be started more than once."); + return; + } + } + + // Return immediately when called from StartAsync; + // continue synchronously when called from RunAsync. + if (runInBackground) + { + await Task.Yield(); + } + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(_stoppedTokenSource.Token, cancellationToken); + var onStartCalled = false; + Exception? exception = null; + try + { + try + { + // Perform start actions. + await OnStartServiceAsync(cts.Token).ConfigureAwait(false); + onStartCalled = true; + + // Check the cancellation token, in case cancellation has been requested + // but StartServiceAsync has not honored the request. + cts.Token.ThrowIfCancellationRequested(); + } + catch + { + _startedCompletionSource.SetResult(false); + throw; + } + + State = AsyncServiceState.Running; + _startedCompletionSource.SetResult(true); + await RunServiceAsync(cts.Token).ConfigureAwait(false); + } + catch (Exception e) when (!e.IsCriticalError()) + { + exception = e; + } + + if (onStartCalled) + { + State = AsyncServiceState.Stopping; + try + { + await OnStopServiceAsync().ConfigureAwait(false); + } + catch (Exception) when (exception is null) + { + throw; + } + catch (Exception e) when (!e.IsCriticalError()) + { + ThrowMultipleExceptions(exception!, e); + } + finally + { + State = AsyncServiceState.Stopped; + } + } + else + { + State = AsyncServiceState.Stopping; + } + + _stoppedCompletionSource.SetResult(true); + if (exception is not null) + { + ExceptionDispatchInfo.Capture(exception).Throw(); + } + } +} diff --git a/src/Louis/Threading/AsyncServiceState.cs b/src/Louis/Threading/AsyncServiceState.cs new file mode 100644 index 0000000..6e11b69 --- /dev/null +++ b/src/Louis/Threading/AsyncServiceState.cs @@ -0,0 +1,40 @@ +// Copyright (c) Tenacom and contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace Louis.Threading; + +/// +/// Represents the state of an . +/// +public enum AsyncServiceState +{ + /// + /// The service has not started yet. + /// + Created, + + /// + /// The service is starting but not functional yet. + /// + Starting, + + /// + /// The service is running. + /// + Running, + + /// + /// The service is stopping. + /// + Stopping, + + /// + /// The service has stopped. + /// + Stopped, + + /// + /// The service has been disposed. + /// + Disposed, +} From 41b1ef45ac80b70c1ecaff2ef09d67884e1869fe Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Tue, 4 Jul 2023 16:21:21 +0200 Subject: [PATCH 02/23] Fix XML docs in AsyncService. --- src/Louis/Threading/AsyncService.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index a5e36f5..87b551b 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -19,15 +19,15 @@ namespace Louis.Threading; /// class found in Microsoft.Extensions.Hosting because it gives you more control over your service: /// /// you can either start your service in background by calling , -/// or run it as a task by calling ; +/// or run it as a task by calling ; /// you can, optionally, override the and methods -/// to separate setup and teardown from the core of your service; +/// to separate setup and teardown from the core of your service; /// you can use the to know, at any time, if your service has been started, has finished starting, -/// is stopping, or has finished stopping; -/// you can synchronize other tasks with your service by calling -/// and ; +/// is stopping, or has finished stopping; +/// you can synchronize other tasks with your service by calling +/// and ; /// you can stop your service and let it complete in the background by calling , -/// or stop and wait for completion by calling . +/// or stop and wait for completion by calling . /// /// public abstract class AsyncService : IAsyncDisposable, IDisposable From 029e77c8cb6ecbfede8c75d6b783ea2029fcefdd Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Tue, 4 Jul 2023 16:23:33 +0200 Subject: [PATCH 03/23] Make overriding OnStartServiceAsync and OnStopServiceAsync optional. --- src/Louis/PublicAPI.Unshipped.txt | 4 ++-- src/Louis/Threading/AsyncService.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Louis/PublicAPI.Unshipped.txt b/src/Louis/PublicAPI.Unshipped.txt index a7d2500..e974f27 100644 --- a/src/Louis/PublicAPI.Unshipped.txt +++ b/src/Louis/PublicAPI.Unshipped.txt @@ -1,6 +1,4 @@ #nullable enable -abstract Louis.Threading.AsyncService.OnStartServiceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask -abstract Louis.Threading.AsyncService.OnStopServiceAsync() -> System.Threading.Tasks.ValueTask abstract Louis.Threading.AsyncService.RunServiceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! const Louis.Diagnostics.ExceptionHelper.NullText = "" -> string! const Louis.Diagnostics.ExceptionHelper.ToStringEmptyText = "" -> string! @@ -189,3 +187,5 @@ static Louis.Threading.ValueTaskUtility.WhenAll(params System.Threading.Tasks.Va static Louis.Threading.ValueTaskUtility.WhenAll(System.Collections.Generic.IEnumerable! valueTasks) -> System.Threading.Tasks.ValueTask static readonly Louis.Text.Utf8Utility.Utf8NoBomEncoding -> System.Text.Encoding! virtual Louis.Threading.AsyncService.OnDisposeServiceAsync() -> System.Threading.Tasks.ValueTask +virtual Louis.Threading.AsyncService.OnStartServiceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +virtual Louis.Threading.AsyncService.OnStopServiceAsync() -> System.Threading.Tasks.ValueTask diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index 87b551b..05ea647 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -225,7 +225,7 @@ public async ValueTask DisposeAsync() /// /// A used to stop the operation. /// A that represents the ongoing operation. - protected abstract ValueTask OnStartServiceAsync(CancellationToken cancellationToken); + protected virtual ValueTask OnStartServiceAsync(CancellationToken cancellationToken) => default; /// /// When overridden in a derived class, performs asynchronous operations @@ -238,7 +238,7 @@ public async ValueTask DisposeAsync() /// (for example if the passed to /// is canceled after checks it for the last time). /// - protected abstract ValueTask OnStopServiceAsync(); + protected virtual ValueTask OnStopServiceAsync() => default; /// /// When overridden in a derived class, performs the actual operations the service is meant to carry out. From 37a9b9db3e136a587efc268ad8023043d0e4cd80 Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Tue, 4 Jul 2023 16:30:25 +0200 Subject: [PATCH 04/23] BRK: Rename core overridable methods in AsyncService, ditching the On* prefix. These methods do not handle events, they do the actual work. --- src/Louis/PublicAPI.Unshipped.txt | 8 ++++---- src/Louis/Threading/AsyncService.cs | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Louis/PublicAPI.Unshipped.txt b/src/Louis/PublicAPI.Unshipped.txt index e974f27..2d856b7 100644 --- a/src/Louis/PublicAPI.Unshipped.txt +++ b/src/Louis/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ #nullable enable -abstract Louis.Threading.AsyncService.RunServiceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +abstract Louis.Threading.AsyncService.ExecuteAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! const Louis.Diagnostics.ExceptionHelper.NullText = "" -> string! const Louis.Diagnostics.ExceptionHelper.ToStringEmptyText = "" -> string! const Louis.Diagnostics.ExceptionHelper.ToStringNullText = "" -> string! @@ -186,6 +186,6 @@ static Louis.Threading.InterlockedFlag.operator ==(Louis.Threading.InterlockedFl static Louis.Threading.ValueTaskUtility.WhenAll(params System.Threading.Tasks.ValueTask[]! valueTasks) -> System.Threading.Tasks.ValueTask static Louis.Threading.ValueTaskUtility.WhenAll(System.Collections.Generic.IEnumerable! valueTasks) -> System.Threading.Tasks.ValueTask static readonly Louis.Text.Utf8Utility.Utf8NoBomEncoding -> System.Text.Encoding! -virtual Louis.Threading.AsyncService.OnDisposeServiceAsync() -> System.Threading.Tasks.ValueTask -virtual Louis.Threading.AsyncService.OnStartServiceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask -virtual Louis.Threading.AsyncService.OnStopServiceAsync() -> System.Threading.Tasks.ValueTask +virtual Louis.Threading.AsyncService.DisposeResourcesAsync() -> System.Threading.Tasks.ValueTask +virtual Louis.Threading.AsyncService.SetupAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +virtual Louis.Threading.AsyncService.TeardownAsync() -> System.Threading.Tasks.ValueTask diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index 05ea647..f57a5bb 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -20,7 +20,7 @@ namespace Louis.Threading; /// /// you can either start your service in background by calling , /// or run it as a task by calling ; -/// you can, optionally, override the and methods +/// you can, optionally, override the and methods /// to separate setup and teardown from the core of your service; /// you can use the to know, at any time, if your service has been started, has finished starting, /// is stopping, or has finished stopping; @@ -162,7 +162,7 @@ public Task StartAsync(CancellationToken cancellationToken) /// /// /// This method stops the service and waits for completion if it has been started, - /// then calls before releasing resources + /// then calls before releasing resources /// held by this class. /// public async ValueTask DisposeAsync() @@ -197,7 +197,7 @@ public async ValueTask DisposeAsync() State = AsyncServiceState.Disposed; } - await OnDisposeServiceAsync().ConfigureAwait(false); + await DisposeResourcesAsync().ConfigureAwait(false); _stoppedTokenSource.Dispose(); } @@ -217,7 +217,7 @@ public async ValueTask DisposeAsync() /// cannot directly own unmanaged resources. /// /// A that represents the ongoing operation. - protected virtual ValueTask OnDisposeServiceAsync() => default; + protected virtual ValueTask DisposeResourcesAsync() => default; /// /// When overridden in a derived class, performs asynchronous operations @@ -225,7 +225,7 @@ public async ValueTask DisposeAsync() /// /// A used to stop the operation. /// A that represents the ongoing operation. - protected virtual ValueTask OnStartServiceAsync(CancellationToken cancellationToken) => default; + protected virtual ValueTask SetupAsync(CancellationToken cancellationToken) => default; /// /// When overridden in a derived class, performs asynchronous operations @@ -238,14 +238,14 @@ public async ValueTask DisposeAsync() /// (for example if the passed to /// is canceled after checks it for the last time). /// - protected virtual ValueTask OnStopServiceAsync() => default; + protected virtual ValueTask TeardownAsync() => default; /// /// When overridden in a derived class, performs the actual operations the service is meant to carry out. /// /// A used to stop the service. /// A representing the ongoing operation. - protected abstract Task RunServiceAsync(CancellationToken cancellationToken); + protected abstract Task ExecuteAsync(CancellationToken cancellationToken); [DoesNotReturn] private static void ThrowOnObjectDisposed(string message) @@ -310,7 +310,7 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella try { // Perform start actions. - await OnStartServiceAsync(cts.Token).ConfigureAwait(false); + await SetupAsync(cts.Token).ConfigureAwait(false); onStartCalled = true; // Check the cancellation token, in case cancellation has been requested @@ -325,7 +325,7 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella State = AsyncServiceState.Running; _startedCompletionSource.SetResult(true); - await RunServiceAsync(cts.Token).ConfigureAwait(false); + await ExecuteAsync(cts.Token).ConfigureAwait(false); } catch (Exception e) when (!e.IsCriticalError()) { @@ -337,7 +337,7 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella State = AsyncServiceState.Stopping; try { - await OnStopServiceAsync().ConfigureAwait(false); + await TeardownAsync().ConfigureAwait(false); } catch (Exception) when (exception is null) { From 8aa6bb3c66e7dd4c50b8a15554d8e0f4124325ce Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Thu, 6 Jul 2023 07:50:16 +0200 Subject: [PATCH 05/23] BRK: Simplify stopping and disposing logic in AsyncService --- src/Louis/PublicAPI.Unshipped.txt | 4 +- src/Louis/Threading/AsyncService.cs | 84 ++++++++++++++--------------- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/src/Louis/PublicAPI.Unshipped.txt b/src/Louis/PublicAPI.Unshipped.txt index 2d856b7..0343eb5 100644 --- a/src/Louis/PublicAPI.Unshipped.txt +++ b/src/Louis/PublicAPI.Unshipped.txt @@ -42,8 +42,8 @@ Louis.Threading.AsyncService.DisposeAsync() -> System.Threading.Tasks.ValueTask Louis.Threading.AsyncService.RunAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Louis.Threading.AsyncService.StartAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Louis.Threading.AsyncService.State.get -> Louis.Threading.AsyncServiceState -Louis.Threading.AsyncService.StopAsync() -> System.Threading.Tasks.Task! -Louis.Threading.AsyncService.TryStop() -> bool +Louis.Threading.AsyncService.Stop() -> bool +Louis.Threading.AsyncService.StopAsync() -> System.Threading.Tasks.Task! Louis.Threading.AsyncService.WaitUntilStartedAsync() -> System.Threading.Tasks.Task! Louis.Threading.AsyncService.WaitUntilStoppedAsync() -> System.Threading.Tasks.Task! Louis.Threading.AsyncServiceState diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index f57a5bb..cfc8076 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -26,7 +26,7 @@ namespace Louis.Threading; /// is stopping, or has finished stopping; /// you can synchronize other tasks with your service by calling /// and ; -/// you can stop your service and let it complete in the background by calling , +/// you can stop your service and let it complete in the background by calling , /// or stop and wait for completion by calling . /// /// @@ -108,28 +108,34 @@ public Task StartAsync(CancellationToken cancellationToken) } /// - /// Asynchronously stops an asynchronous service. + /// Stops an asynchronous service, without waiting for it to complete. + /// If the service has not started yet, calling this method prevents it from starting. /// /// - /// A that will complete as soon as the service has finished stopping. - /// If the service has already stopped, the returned task is already completed. + /// if the service had started and has been stopped; + /// if the service had not started yet, . /// - /// - /// The service has not been started yet. - /// - public Task StopAsync() => TryStop() ? WaitUntilStoppedAsync() : Task.CompletedTask; + public bool Stop() => StopCore(false); /// - /// Tries to stop an asynchronous service. + /// Asynchronously stops an asynchronous service and waits for it to complete. + /// If the service has not started yet, calling this method prevents it from starting. /// /// - /// if the service has been stopped; - /// if it had already stopped. + /// A Task<bool> that will complete as soon as the service has finished stopping + /// (in which case the result will be ), or immediately if the service had not started yet + /// (in which case the result will be ). /// - /// - /// The service has not been started yet. - /// - public bool TryStop() => TryStopCore(out var exception) || (exception is null ? false : throw exception); + public async Task StopAsync() + { + if (!StopCore(false)) + { + return false; + } + + await WaitUntilStoppedAsync().ConfigureAwait(false); + return true; + } /// /// Asynchronously wait until an asynchronous service has started. @@ -167,7 +173,7 @@ public Task StartAsync(CancellationToken cancellationToken) /// public async ValueTask DisposeAsync() { - AsyncServiceState previousState; + bool hadStarted; lock (_stateSyncRoot) { if (!_disposed.TrySet()) @@ -175,26 +181,12 @@ public async ValueTask DisposeAsync() return; } - previousState = _state; - if (previousState < AsyncServiceState.Starting) - { - _state = AsyncServiceState.Disposed; - } + hadStarted = StopCore(true); } - if (previousState < AsyncServiceState.Starting) + if (hadStarted) { - _ = _startedCompletionSource.TrySetResult(false); - _ = _stoppedCompletionSource.TrySetResult(true); - } - else - { - if (TryStopCore(out var exception) && exception is null) - { - await WaitUntilStoppedAsync().ConfigureAwait(false); - } - - State = AsyncServiceState.Disposed; + await WaitUntilStoppedAsync().ConfigureAwait(false); } await DisposeResourcesAsync().ConfigureAwait(false); @@ -255,26 +247,32 @@ private static void ThrowOnObjectDisposed(string message) private static void ThrowMultipleExceptions(params Exception[] innerExceptions) => throw new AggregateException(innerExceptions); - private bool TryStopCore(out Exception? exception) + private bool StopCore(bool disposing) { lock (_stateSyncRoot) { switch (_state) { case < AsyncServiceState.Starting: - exception = new InvalidOperationException("The service has not been started yet."); + _ = _startedCompletionSource.TrySetResult(false); + _ = _stoppedCompletionSource.TrySetResult(true); + _state = disposing ? AsyncServiceState.Disposed : AsyncServiceState.Stopped; return false; + case > AsyncServiceState.Running: - exception = null; - return false; - } + if (disposing) + { + _state = AsyncServiceState.Disposed; + } - _state = AsyncServiceState.Stopping; - } + return true; - _stoppedTokenSource.Cancel(); - exception = null; - return true; + default: + _state = disposing ? AsyncServiceState.Disposed : AsyncServiceState.Stopping; + _stoppedTokenSource.Cancel(); + return true; + } + } } private async Task RunAsyncCore(bool runInBackground, CancellationToken cancellationToken) From effbff1f37fa1edce4f0c9cd7b138188ca12f63d Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Thu, 6 Jul 2023 22:07:30 +0200 Subject: [PATCH 06/23] Partially refactor AsyncService: - reduce the number of nested try blocks in RunAsyncCore - add a synchronous Start method, semantically consistent with Stop - use a private method to modify _state --- src/Louis/PublicAPI.Unshipped.txt | 1 + src/Louis/Threading/AsyncService.cs | 165 ++++++++++++++++++---------- 2 files changed, 111 insertions(+), 55 deletions(-) diff --git a/src/Louis/PublicAPI.Unshipped.txt b/src/Louis/PublicAPI.Unshipped.txt index 0343eb5..e206104 100644 --- a/src/Louis/PublicAPI.Unshipped.txt +++ b/src/Louis/PublicAPI.Unshipped.txt @@ -40,6 +40,7 @@ Louis.Threading.AsyncService.AsyncService() -> void Louis.Threading.AsyncService.Dispose() -> void Louis.Threading.AsyncService.DisposeAsync() -> System.Threading.Tasks.ValueTask Louis.Threading.AsyncService.RunAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Louis.Threading.AsyncService.Start(System.Threading.CancellationToken cancellationToken) -> void Louis.Threading.AsyncService.StartAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Louis.Threading.AsyncService.State.get -> Louis.Threading.AsyncServiceState Louis.Threading.AsyncService.Stop() -> bool diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index cfc8076..22600bf 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -18,12 +18,13 @@ namespace Louis.Threading; /// This class differs from the BackgroundService /// class found in Microsoft.Extensions.Hosting because it gives you more control over your service: /// -/// you can either start your service in background by calling , -/// or run it as a task by calling ; /// you can, optionally, override the and methods /// to separate setup and teardown from the core of your service; -/// you can use the to know, at any time, if your service has been started, has finished starting, -/// is stopping, or has finished stopping; +/// you can either start your service in background by calling , +/// start it in background and wait for preliminary operations to complete by calling , +/// or run it as a task by calling ; +/// you can use the property to know, at any time, if your service has been started, has finished starting, +/// is stopping, has finished stopping, or has been disposed; /// you can synchronize other tasks with your service by calling /// and ; /// you can stop your service and let it complete in the background by calling , @@ -64,7 +65,7 @@ private set { lock (_stateSyncRoot) { - _state = value; + UnsafeSetState(value); } } } @@ -77,15 +78,30 @@ private set /// A that will complete when the service has stopped. /// /// - /// The service has already been started, either by calling or . + /// The service has already been started, either by calling , , + /// or . /// /// - /// You may call one of and at most once. + /// Only one of , , and may be called, at most once. /// public Task RunAsync(CancellationToken cancellationToken) => RunAsyncCore(false, cancellationToken); /// - /// Asynchronously start an asynchronous service, then return while it continues to run in the background. + /// Starts an asynchronous service, then return while it continues to run in the background. + /// + /// A used to stop the service. + /// + /// The service has already been started, either by calling , , or . + /// + /// + /// Only one of , , and may be called, at most once. + /// If your program needs to know the exact reason why a service stops or fails to start, + /// do not use this method; call from a separate task instead. + /// + public void Start(CancellationToken cancellationToken) => _ = RunAsyncCore(true, cancellationToken); + + /// + /// Asynchronously starts an asynchronous service and waits for its method to complete. /// /// A used to stop the service. /// @@ -94,12 +110,12 @@ private set /// or the cancellation of (in which case the result will be ). /// /// - /// The service has already been started, either by calling or . + /// The service has already been started, either by calling , , or . /// /// /// You may call one of and at most once. /// If your program needs to know the exact reason why a service stops or fails to start, - /// do not use this method; call instead. + /// do not use this method; call from a separate task instead. /// public Task StartAsync(CancellationToken cancellationToken) { @@ -112,8 +128,8 @@ public Task StartAsync(CancellationToken cancellationToken) /// If the service has not started yet, calling this method prevents it from starting. /// /// - /// if the service had started and has been stopped; - /// if the service had not started yet, . + /// if the service was running and has been requested to stop; + /// if the service was not running. /// public bool Stop() => StopCore(false); @@ -123,7 +139,7 @@ public Task StartAsync(CancellationToken cancellationToken) /// /// /// A Task<bool> that will complete as soon as the service has finished stopping - /// (in which case the result will be ), or immediately if the service had not started yet + /// (in which case the result will be ), or immediately if the service was not running /// (in which case the result will be ). /// public async Task StopAsync() @@ -239,10 +255,30 @@ public async ValueTask DisposeAsync() /// A representing the ongoing operation. protected abstract Task ExecuteAsync(CancellationToken cancellationToken); + [DoesNotReturn] private static void ThrowOnObjectDisposed(string message) => throw new ObjectDisposedException(message); + private static void AggregateAndThrowIfNeeded(Exception? exception1, Exception? exception2) + { + if (exception1 is { }) + { + if (exception2 is { }) + { + ThrowMultipleExceptions(exception1, exception2); + } + else + { + ExceptionDispatchInfo.Capture(exception1).Throw(); + } + } + else if (exception2 is { }) + { + ExceptionDispatchInfo.Capture(exception2).Throw(); + } + } + [DoesNotReturn] private static void ThrowMultipleExceptions(params Exception[] innerExceptions) => throw new AggregateException(innerExceptions); @@ -256,19 +292,22 @@ private bool StopCore(bool disposing) case < AsyncServiceState.Starting: _ = _startedCompletionSource.TrySetResult(false); _ = _stoppedCompletionSource.TrySetResult(true); - _state = disposing ? AsyncServiceState.Disposed : AsyncServiceState.Stopped; + UnsafeSetState(disposing ? AsyncServiceState.Disposed : AsyncServiceState.Stopped); + return false; + + case AsyncServiceState.Disposed: return false; case > AsyncServiceState.Running: if (disposing) { - _state = AsyncServiceState.Disposed; + UnsafeSetState(AsyncServiceState.Disposed); } return true; default: - _state = disposing ? AsyncServiceState.Disposed : AsyncServiceState.Stopping; + UnsafeSetState(disposing ? AsyncServiceState.Disposed : AsyncServiceState.Stopping); _stoppedTokenSource.Cancel(); return true; } @@ -282,7 +321,7 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella switch (_state) { case AsyncServiceState.Created: - _state = AsyncServiceState.Starting; + UnsafeSetState(AsyncServiceState.Starting); break; case AsyncServiceState.Disposed: ThrowOnObjectDisposed("Trying to run a disposed async service."); @@ -293,7 +332,7 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella } } - // Return immediately when called from StartAsync; + // Return immediately when called from Start or StartAsync; // continue synchronously when called from RunAsync. if (runInBackground) { @@ -301,64 +340,80 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella } using var cts = CancellationTokenSource.CreateLinkedTokenSource(_stoppedTokenSource.Token, cancellationToken); - var onStartCalled = false; + var setupCompleted = false; Exception? exception = null; try { - try - { - // Perform start actions. - await SetupAsync(cts.Token).ConfigureAwait(false); - onStartCalled = true; + // Perform start actions. + await SetupAsync(cts.Token).ConfigureAwait(false); - // Check the cancellation token, in case cancellation has been requested - // but StartServiceAsync has not honored the request. - cts.Token.ThrowIfCancellationRequested(); - } - catch + // Check the cancellation token, in case cancellation has been requested + // but SetupAsync has not honored the request. + cts.Token.ThrowIfCancellationRequested(); + + setupCompleted = true; + } + catch (OperationCanceledException) when (cts.IsCancellationRequested) + { + } + catch (Exception e) when (!e.IsCriticalError()) + { + exception = e; + } + + if (!setupCompleted) + { + _startedCompletionSource.SetResult(false); + _stoppedCompletionSource.SetResult(true); + State = AsyncServiceState.Stopped; + + // Only propagate exceptions if there is a caller to propagate to. + if (!runInBackground && exception is not null) { - _startedCompletionSource.SetResult(false); - throw; + ExceptionDispatchInfo.Capture(exception).Throw(); } - State = AsyncServiceState.Running; - _startedCompletionSource.SetResult(true); + return; + } + + State = AsyncServiceState.Running; + _startedCompletionSource.SetResult(true); + try + { await ExecuteAsync(cts.Token).ConfigureAwait(false); } + catch (OperationCanceledException) when (cts.IsCancellationRequested) + { + } catch (Exception e) when (!e.IsCriticalError()) { exception = e; } - if (onStartCalled) + Exception? teardownException = null; + State = AsyncServiceState.Stopping; + try { - State = AsyncServiceState.Stopping; - try - { - await TeardownAsync().ConfigureAwait(false); - } - catch (Exception) when (exception is null) - { - throw; - } - catch (Exception e) when (!e.IsCriticalError()) - { - ThrowMultipleExceptions(exception!, e); - } - finally - { - State = AsyncServiceState.Stopped; - } + await TeardownAsync().ConfigureAwait(false); } - else + catch (Exception e) when (!e.IsCriticalError()) { - State = AsyncServiceState.Stopping; + teardownException = e; } + State = AsyncServiceState.Stopped; _stoppedCompletionSource.SetResult(true); - if (exception is not null) + AggregateAndThrowIfNeeded(exception, teardownException); + } + + private void UnsafeSetState(AsyncServiceState value) + { + if (value == _state) { - ExceptionDispatchInfo.Capture(exception).Throw(); + return; } + + var oldState = _state; + _state = value; } } From 1b86faf9c23e74efec506f7a6084469288eaa0d5 Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Thu, 6 Jul 2023 22:13:46 +0200 Subject: [PATCH 07/23] ADD: AsyncService.DoneToken to monitor execution from a dependent service --- src/Louis/PublicAPI.Unshipped.txt | 1 + src/Louis/Threading/AsyncService.cs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Louis/PublicAPI.Unshipped.txt b/src/Louis/PublicAPI.Unshipped.txt index e206104..f069867 100644 --- a/src/Louis/PublicAPI.Unshipped.txt +++ b/src/Louis/PublicAPI.Unshipped.txt @@ -39,6 +39,7 @@ Louis.Threading.AsyncService Louis.Threading.AsyncService.AsyncService() -> void Louis.Threading.AsyncService.Dispose() -> void Louis.Threading.AsyncService.DisposeAsync() -> System.Threading.Tasks.ValueTask +Louis.Threading.AsyncService.DoneToken.get -> System.Threading.CancellationToken Louis.Threading.AsyncService.RunAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Louis.Threading.AsyncService.Start(System.Threading.CancellationToken cancellationToken) -> void Louis.Threading.AsyncService.StartAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index 22600bf..3f84a16 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -25,6 +25,8 @@ namespace Louis.Threading; /// or run it as a task by calling ; /// you can use the property to know, at any time, if your service has been started, has finished starting, /// is stopping, has finished stopping, or has been disposed; +/// you can use the property to get a cancellation token that will be canceled +/// as soon as execution of the service stops, just before the teardown phase starts; /// you can synchronize other tasks with your service by calling /// and ; /// you can stop your service and let it complete in the background by calling , @@ -34,6 +36,7 @@ namespace Louis.Threading; public abstract class AsyncService : IAsyncDisposable, IDisposable { private readonly CancellationTokenSource _stoppedTokenSource = new(); + private readonly CancellationTokenSource _doneTokenSource = new(); private readonly TaskCompletionSource _startedCompletionSource = new(); private readonly TaskCompletionSource _stoppedCompletionSource = new(); private readonly object _stateSyncRoot = new(); @@ -71,7 +74,13 @@ private set } /// - /// Asynchronously run an asynchronous service. + /// Gets a that is canceled as soon as the service has finished executing + /// (either successfully or with an exception) or has failed starting. + /// + public CancellationToken DoneToken => _doneTokenSource.Token; + + /// + /// Asynchronously runs an asynchronous service. /// /// A used to stop the service. /// @@ -365,6 +374,7 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella { _startedCompletionSource.SetResult(false); _stoppedCompletionSource.SetResult(true); + _doneTokenSource.Cancel(); State = AsyncServiceState.Stopped; // Only propagate exceptions if there is a caller to propagate to. @@ -391,6 +401,7 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella } Exception? teardownException = null; + _doneTokenSource.Cancel(); State = AsyncServiceState.Stopping; try { From 97f2568f7c8f7a7077f218c99e3827d929427485 Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Thu, 6 Jul 2023 22:14:11 +0200 Subject: [PATCH 08/23] ADD: Logging hooks in AsyncService. --- src/Louis/PublicAPI.Unshipped.txt | 6 +++ src/Louis/Threading/AsyncService.cs | 62 +++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/src/Louis/PublicAPI.Unshipped.txt b/src/Louis/PublicAPI.Unshipped.txt index f069867..a2ddbab 100644 --- a/src/Louis/PublicAPI.Unshipped.txt +++ b/src/Louis/PublicAPI.Unshipped.txt @@ -189,5 +189,11 @@ static Louis.Threading.ValueTaskUtility.WhenAll(params System.Threading.Tasks.Va static Louis.Threading.ValueTaskUtility.WhenAll(System.Collections.Generic.IEnumerable! valueTasks) -> System.Threading.Tasks.ValueTask static readonly Louis.Text.Utf8Utility.Utf8NoBomEncoding -> System.Text.Encoding! virtual Louis.Threading.AsyncService.DisposeResourcesAsync() -> System.Threading.Tasks.ValueTask +virtual Louis.Threading.AsyncService.OnExecuteCanceled() -> void +virtual Louis.Threading.AsyncService.OnExecuteFailed(System.Exception! exception) -> void +virtual Louis.Threading.AsyncService.OnSetupCanceled() -> void +virtual Louis.Threading.AsyncService.OnSetupFailed(System.Exception! exception) -> void +virtual Louis.Threading.AsyncService.OnStateChanged(Louis.Threading.AsyncServiceState oldState, Louis.Threading.AsyncServiceState newState) -> void +virtual Louis.Threading.AsyncService.OnTeardownFailed(System.Exception! exception) -> void virtual Louis.Threading.AsyncService.SetupAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask virtual Louis.Threading.AsyncService.TeardownAsync() -> System.Threading.Tasks.ValueTask diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index 3f84a16..df37eb3 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -264,6 +264,62 @@ public async ValueTask DisposeAsync() /// A representing the ongoing operation. protected abstract Task ExecuteAsync(CancellationToken cancellationToken); + /// + /// Called whenever the property changes. + /// This method must return as early as possible, must not throw, and should be only used for logging purposes. + /// + /// The old value of . + /// The new value of . + protected virtual void OnStateChanged(AsyncServiceState oldState, AsyncServiceState newState) + { + } + + /// + /// Called if the method throws + /// and the service has been requested to stop. + /// This method must return as early as possible, must not throw, and should be only used for logging purposes. + /// + protected virtual void OnSetupCanceled() + { + } + + /// + /// Called if the method throws an exception, except when such exception indicates + /// that service execution has been canceled. + /// This method must return as early as possible, must not throw, and should be only used for logging purposes. + /// + /// The exception thrown by . + protected virtual void OnSetupFailed(Exception exception) + { + } + + /// + /// Called if the method throws + /// and the service has been requested to stop. + /// This method must return as early as possible, must not throw, and should be only used for logging purposes. + /// + protected virtual void OnExecuteCanceled() + { + } + + /// + /// Called if the method throws an exception, except when such exception indicates + /// that service execution has been canceled. + /// This method must return as early as possible, must not throw, and should be only used for logging purposes. + /// + /// The exception thrown by . + protected virtual void OnExecuteFailed(Exception exception) + { + } + + /// + /// Called if the method throws an exception. + /// This method must return as early as possible, must not throw, and should be only used for logging purposes. + /// + /// The exception thrown by . + protected virtual void OnTeardownFailed(Exception exception) + { + } [DoesNotReturn] private static void ThrowOnObjectDisposed(string message) @@ -364,9 +420,11 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella } catch (OperationCanceledException) when (cts.IsCancellationRequested) { + OnSetupCanceled(); } catch (Exception e) when (!e.IsCriticalError()) { + OnSetupFailed(e); exception = e; } @@ -394,9 +452,11 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella } catch (OperationCanceledException) when (cts.IsCancellationRequested) { + OnExecuteCanceled(); } catch (Exception e) when (!e.IsCriticalError()) { + OnExecuteFailed(e); exception = e; } @@ -409,6 +469,7 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella } catch (Exception e) when (!e.IsCriticalError()) { + OnTeardownFailed(e); teardownException = e; } @@ -426,5 +487,6 @@ private void UnsafeSetState(AsyncServiceState value) var oldState = _state; _state = value; + OnStateChanged(oldState, _state); } } From f97f98a1c98c0d90d132ec0a99549a9622bc8a22 Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Fri, 7 Jul 2023 00:31:08 +0200 Subject: [PATCH 09/23] BRK: Rename logging hooks in AsyncService to make their purpose clear --- src/Louis/PublicAPI.Unshipped.txt | 12 ++++++------ src/Louis/Threading/AsyncService.cs | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Louis/PublicAPI.Unshipped.txt b/src/Louis/PublicAPI.Unshipped.txt index a2ddbab..38ae902 100644 --- a/src/Louis/PublicAPI.Unshipped.txt +++ b/src/Louis/PublicAPI.Unshipped.txt @@ -189,11 +189,11 @@ static Louis.Threading.ValueTaskUtility.WhenAll(params System.Threading.Tasks.Va static Louis.Threading.ValueTaskUtility.WhenAll(System.Collections.Generic.IEnumerable! valueTasks) -> System.Threading.Tasks.ValueTask static readonly Louis.Text.Utf8Utility.Utf8NoBomEncoding -> System.Text.Encoding! virtual Louis.Threading.AsyncService.DisposeResourcesAsync() -> System.Threading.Tasks.ValueTask -virtual Louis.Threading.AsyncService.OnExecuteCanceled() -> void -virtual Louis.Threading.AsyncService.OnExecuteFailed(System.Exception! exception) -> void -virtual Louis.Threading.AsyncService.OnSetupCanceled() -> void -virtual Louis.Threading.AsyncService.OnSetupFailed(System.Exception! exception) -> void -virtual Louis.Threading.AsyncService.OnStateChanged(Louis.Threading.AsyncServiceState oldState, Louis.Threading.AsyncServiceState newState) -> void -virtual Louis.Threading.AsyncService.OnTeardownFailed(System.Exception! exception) -> void +virtual Louis.Threading.AsyncService.LogExecuteCanceled() -> void +virtual Louis.Threading.AsyncService.LogExecuteFailed(System.Exception! exception) -> void +virtual Louis.Threading.AsyncService.LogSetupCanceled() -> void +virtual Louis.Threading.AsyncService.LogSetupFailed(System.Exception! exception) -> void +virtual Louis.Threading.AsyncService.LogStateChanged(Louis.Threading.AsyncServiceState oldState, Louis.Threading.AsyncServiceState newState) -> void +virtual Louis.Threading.AsyncService.LogTeardownFailed(System.Exception! exception) -> void virtual Louis.Threading.AsyncService.SetupAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask virtual Louis.Threading.AsyncService.TeardownAsync() -> System.Threading.Tasks.ValueTask diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index df37eb3..e287805 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -270,7 +270,7 @@ public async ValueTask DisposeAsync() /// /// The old value of . /// The new value of . - protected virtual void OnStateChanged(AsyncServiceState oldState, AsyncServiceState newState) + protected virtual void LogStateChanged(AsyncServiceState oldState, AsyncServiceState newState) { } @@ -279,7 +279,7 @@ protected virtual void OnStateChanged(AsyncServiceState oldState, AsyncServiceSt /// and the service has been requested to stop. /// This method must return as early as possible, must not throw, and should be only used for logging purposes. /// - protected virtual void OnSetupCanceled() + protected virtual void LogSetupCanceled() { } @@ -289,7 +289,7 @@ protected virtual void OnSetupCanceled() /// This method must return as early as possible, must not throw, and should be only used for logging purposes. /// /// The exception thrown by . - protected virtual void OnSetupFailed(Exception exception) + protected virtual void LogSetupFailed(Exception exception) { } @@ -298,7 +298,7 @@ protected virtual void OnSetupFailed(Exception exception) /// and the service has been requested to stop. /// This method must return as early as possible, must not throw, and should be only used for logging purposes. /// - protected virtual void OnExecuteCanceled() + protected virtual void LogExecuteCanceled() { } @@ -308,7 +308,7 @@ protected virtual void OnExecuteCanceled() /// This method must return as early as possible, must not throw, and should be only used for logging purposes. /// /// The exception thrown by . - protected virtual void OnExecuteFailed(Exception exception) + protected virtual void LogExecuteFailed(Exception exception) { } @@ -317,7 +317,7 @@ protected virtual void OnExecuteFailed(Exception exception) /// This method must return as early as possible, must not throw, and should be only used for logging purposes. /// /// The exception thrown by . - protected virtual void OnTeardownFailed(Exception exception) + protected virtual void LogTeardownFailed(Exception exception) { } @@ -420,11 +420,11 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella } catch (OperationCanceledException) when (cts.IsCancellationRequested) { - OnSetupCanceled(); + LogSetupCanceled(); } catch (Exception e) when (!e.IsCriticalError()) { - OnSetupFailed(e); + LogSetupFailed(e); exception = e; } @@ -452,11 +452,11 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella } catch (OperationCanceledException) when (cts.IsCancellationRequested) { - OnExecuteCanceled(); + LogExecuteCanceled(); } catch (Exception e) when (!e.IsCriticalError()) { - OnExecuteFailed(e); + LogExecuteFailed(e); exception = e; } @@ -469,7 +469,7 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella } catch (Exception e) when (!e.IsCriticalError()) { - OnTeardownFailed(e); + LogTeardownFailed(e); teardownException = e; } @@ -487,6 +487,6 @@ private void UnsafeSetState(AsyncServiceState value) var oldState = _state; _state = value; - OnStateChanged(oldState, _state); + LogStateChanged(oldState, _state); } } From 6895d9b87379f75deddda88c7b2aa66994cc7e3e Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Fri, 7 Jul 2023 00:37:31 +0200 Subject: [PATCH 10/23] Add Louis.Hosting project --- Directory.Packages.props | 1 + Louis.sln | 7 +++++++ src/Louis.Hosting/Louis.Hosting.csproj | 17 +++++++++++++++++ src/Louis.Hosting/NuGet-README.md | 10 ++++++++++ src/Louis.Hosting/PublicAPI.Shipped.txt | 1 + src/Louis.Hosting/PublicAPI.Unshipped.txt | 1 + 6 files changed, 37 insertions(+) create mode 100644 src/Louis.Hosting/Louis.Hosting.csproj create mode 100644 src/Louis.Hosting/NuGet-README.md create mode 100644 src/Louis.Hosting/PublicAPI.Shipped.txt create mode 100644 src/Louis.Hosting/PublicAPI.Unshipped.txt diff --git a/Directory.Packages.props b/Directory.Packages.props index 15363c0..1eb94e7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,6 +14,7 @@ + diff --git a/Louis.sln b/Louis.sln index 27ec409..5bf7685 100644 --- a/Louis.sln +++ b/Louis.sln @@ -48,6 +48,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "- Dependencies", "- Depende EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoggingTests", "tests\LoggingTests\LoggingTests.csproj", "{F6604E25-C15E-4108-AE40-4C209767C473}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Louis.Hosting", "src\Louis.Hosting\Louis.Hosting.csproj", "{1962DC3E-CA58-44E3-8186-7E42D8A9646E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -66,6 +68,10 @@ Global {F6604E25-C15E-4108-AE40-4C209767C473}.Debug|Any CPU.Build.0 = Debug|Any CPU {F6604E25-C15E-4108-AE40-4C209767C473}.Release|Any CPU.ActiveCfg = Release|Any CPU {F6604E25-C15E-4108-AE40-4C209767C473}.Release|Any CPU.Build.0 = Release|Any CPU + {1962DC3E-CA58-44E3-8186-7E42D8A9646E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1962DC3E-CA58-44E3-8186-7E42D8A9646E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1962DC3E-CA58-44E3-8186-7E42D8A9646E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1962DC3E-CA58-44E3-8186-7E42D8A9646E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -75,6 +81,7 @@ Global {E9C789C3-E611-4BF1-BCEF-73F2F4BF13BB} = {75CB1268-E052-4A3F-8022-2C21C84D9B32} {9673D9AD-53CE-4768-A74D-E8719A9D8FBD} = {AF6E590F-F997-442E-8F77-356C7E4DF275} {F6604E25-C15E-4108-AE40-4C209767C473} = {0977BFBE-C5F3-4B90-9F9E-77061320CB9D} + {1962DC3E-CA58-44E3-8186-7E42D8A9646E} = {AF6E590F-F997-442E-8F77-356C7E4DF275} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {552BE595-AF8A-4BA2-9F45-D3CED6737ABB} diff --git a/src/Louis.Hosting/Louis.Hosting.csproj b/src/Louis.Hosting/Louis.Hosting.csproj new file mode 100644 index 0000000..9c026b4 --- /dev/null +++ b/src/Louis.Hosting/Louis.Hosting.csproj @@ -0,0 +1,17 @@ + + + + Integration of L.o.U.I.S. with Microsoft.Extensions.Hosting. + netstandard2.0;netstandard2.1;net462;net6.0;net7.0 + false + + + + + + + + + + + diff --git a/src/Louis.Hosting/NuGet-README.md b/src/Louis.Hosting/NuGet-README.md new file mode 100644 index 0000000..101dfaa --- /dev/null +++ b/src/Louis.Hosting/NuGet-README.md @@ -0,0 +1,10 @@ +# ![L.o.U.I.S.](https://raw.githubusercontent.com/Tenacom/Louis/main/graphics/Readme.png) + +--- + +L.o.U.I.S. (pronounced _LOO-iss_, just like the name Louis) is a collection of useful types commonly needed in the development of .NET libraries and applications. + +This package contains `Louis.Hosting.dll`, that integrates L.o.U.I.S. with Microsoft's hosting extensions. + +Want to know more? [Here's the complete README.](https://github.com/Tenacom/Louis#readme) + diff --git a/src/Louis.Hosting/PublicAPI.Shipped.txt b/src/Louis.Hosting/PublicAPI.Shipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/Louis.Hosting/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Louis.Hosting/PublicAPI.Unshipped.txt b/src/Louis.Hosting/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/Louis.Hosting/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable From 242846077e8c07ada620b2f16e67ddf96e135a1d Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Fri, 7 Jul 2023 00:38:33 +0200 Subject: [PATCH 11/23] ADD: AsyncHostedService, a subclass of AsyncService that implements IHostedService --- src/Louis.Hosting/AsyncHostedService.cs | 89 +++++++++++++++++++++++ src/Louis.Hosting/Internal/EventIds.cs | 17 +++++ src/Louis.Hosting/Louis.Hosting.csproj | 2 + src/Louis.Hosting/PublicAPI.Unshipped.txt | 8 ++ 4 files changed, 116 insertions(+) create mode 100644 src/Louis.Hosting/AsyncHostedService.cs create mode 100644 src/Louis.Hosting/Internal/EventIds.cs diff --git a/src/Louis.Hosting/AsyncHostedService.cs b/src/Louis.Hosting/AsyncHostedService.cs new file mode 100644 index 0000000..75259b5 --- /dev/null +++ b/src/Louis.Hosting/AsyncHostedService.cs @@ -0,0 +1,89 @@ +// Copyright (c) Tenacom and contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Diagnostics; +using Louis.Hosting.Internal; +using Louis.Threading; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Louis.Hosting; + +/// +/// Subclasses , providing support for logging and implementing . +/// +public abstract partial class AsyncHostedService : AsyncService, IHostedService +{ + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// An to use for logging. + protected AsyncHostedService(ILogger logger) + { + Guard.IsNotNull(logger); + _logger = logger; + } + + /// + async Task IHostedService.StartAsync(CancellationToken cancellationToken) + { + if (await StartAsync(cancellationToken)) + { + return; + } + + // If AsyncService.StartAsync returns false, then either the service was canceled, or SetupAsync faulted. + cancellationToken.ThrowIfCancellationRequested(); + ThrowHelper.ThrowInvalidOperationException("Service start faulted."); + } + + /// + Task IHostedService.StopAsync(CancellationToken cancellationToken) => Task.WhenAny(StopAsync(), Task.Delay(Timeout.Infinite, cancellationToken)); + + /// + [LoggerMessage( + eventId: EventIds.AsyncHostedService.StateChanged, + level: LogLevel.Trace, + message: "Service state {oldState} -> {newState}")] + protected sealed override partial void LogStateChanged(AsyncServiceState oldState, AsyncServiceState newState); + + /// + [LoggerMessage( + eventId: EventIds.AsyncHostedService.SetupCanceled, + level: LogLevel.Warning, + message: "Service execution was canceled during setup phase")] + protected sealed override partial void LogSetupCanceled(); + + /// + [LoggerMessage( + eventId: EventIds.AsyncHostedService.SetupFailed, + level: LogLevel.Error, + message: "Service setup phase failed")] + protected sealed override partial void LogSetupFailed(Exception exception); + + /// + [LoggerMessage( + eventId: EventIds.AsyncHostedService.ExecuteCanceled, + level: LogLevel.Warning, + message: "Service execution was canceled")] + protected sealed override partial void LogExecuteCanceled(); + + /// + [LoggerMessage( + eventId: EventIds.AsyncHostedService.ExecuteFailed, + level: LogLevel.Error, + message: "Service execution failed")] + protected sealed override partial void LogExecuteFailed(Exception exception); + + /// + [LoggerMessage( + eventId: EventIds.AsyncHostedService.TeardownFailed, + level: LogLevel.Error, + message: "Service teardown phase failed")] + protected sealed override partial void LogTeardownFailed(Exception exception); +} diff --git a/src/Louis.Hosting/Internal/EventIds.cs b/src/Louis.Hosting/Internal/EventIds.cs new file mode 100644 index 0000000..3050267 --- /dev/null +++ b/src/Louis.Hosting/Internal/EventIds.cs @@ -0,0 +1,17 @@ +// Copyright (c) Tenacom and contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace Louis.Hosting.Internal; + +internal static class EventIds +{ + public static class AsyncHostedService + { + public const int StateChanged = 0; + public const int SetupCanceled = 1; + public const int SetupFailed = 2; + public const int ExecuteCanceled = 3; + public const int ExecuteFailed = 4; + public const int TeardownFailed = 5; + } +} diff --git a/src/Louis.Hosting/Louis.Hosting.csproj b/src/Louis.Hosting/Louis.Hosting.csproj index 9c026b4..e04189a 100644 --- a/src/Louis.Hosting/Louis.Hosting.csproj +++ b/src/Louis.Hosting/Louis.Hosting.csproj @@ -11,7 +11,9 @@ + + diff --git a/src/Louis.Hosting/PublicAPI.Unshipped.txt b/src/Louis.Hosting/PublicAPI.Unshipped.txt index 7dc5c58..e76f850 100644 --- a/src/Louis.Hosting/PublicAPI.Unshipped.txt +++ b/src/Louis.Hosting/PublicAPI.Unshipped.txt @@ -1 +1,9 @@ #nullable enable +Louis.Hosting.AsyncHostedService +Louis.Hosting.AsyncHostedService.AsyncHostedService(Microsoft.Extensions.Logging.ILogger! logger) -> void +override sealed Louis.Hosting.AsyncHostedService.LogExecuteCanceled() -> void +override sealed Louis.Hosting.AsyncHostedService.LogExecuteFailed(System.Exception! exception) -> void +override sealed Louis.Hosting.AsyncHostedService.LogSetupCanceled() -> void +override sealed Louis.Hosting.AsyncHostedService.LogSetupFailed(System.Exception! exception) -> void +override sealed Louis.Hosting.AsyncHostedService.LogStateChanged(Louis.Threading.AsyncServiceState oldState, Louis.Threading.AsyncServiceState newState) -> void +override sealed Louis.Hosting.AsyncHostedService.LogTeardownFailed(System.Exception! exception) -> void From 82f61c526a884ed0d2ecf47badd7a106511a7252 Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Fri, 7 Jul 2023 02:03:50 +0200 Subject: [PATCH 12/23] Improve readability of some null checks in AsyncService --- src/Louis/Threading/AsyncService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index e287805..580a362 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -327,9 +327,9 @@ private static void ThrowOnObjectDisposed(string message) private static void AggregateAndThrowIfNeeded(Exception? exception1, Exception? exception2) { - if (exception1 is { }) + if (exception1 is not null) { - if (exception2 is { }) + if (exception2 is not null) { ThrowMultipleExceptions(exception1, exception2); } @@ -338,7 +338,7 @@ private static void AggregateAndThrowIfNeeded(Exception? exception1, Exception? ExceptionDispatchInfo.Capture(exception1).Throw(); } } - else if (exception2 is { }) + else if (exception2 is not null) { ExceptionDispatchInfo.Capture(exception2).Throw(); } @@ -436,7 +436,7 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella State = AsyncServiceState.Stopped; // Only propagate exceptions if there is a caller to propagate to. - if (!runInBackground && exception is not null) + if (exception is not null && !runInBackground) { ExceptionDispatchInfo.Capture(exception).Throw(); } From 3374dfce47664e4c3f920e943ff42eb2d1cf0184 Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Fri, 7 Jul 2023 02:05:03 +0200 Subject: [PATCH 13/23] Do not call AsyncService.SetupAsync and ExecuteAsync if cancellation already requested --- src/Louis/Threading/AsyncService.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index 580a362..92a4e57 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -409,10 +409,13 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella Exception? exception = null; try { + // Check the cancellation token first, in case it is already canceled. + cts.Token.ThrowIfCancellationRequested(); + // Perform start actions. await SetupAsync(cts.Token).ConfigureAwait(false); - // Check the cancellation token, in case cancellation has been requested + // Check the cancellation token again, in case cancellation has been requested // but SetupAsync has not honored the request. cts.Token.ThrowIfCancellationRequested(); @@ -448,7 +451,15 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella _startedCompletionSource.SetResult(true); try { + // Check the cancellation token first, in case it is already canceled. + cts.Token.ThrowIfCancellationRequested(); + + // Execute the service. await ExecuteAsync(cts.Token).ConfigureAwait(false); + + // Check the cancellation token again, in case cancellation has been requested + // but ExecuteAsync has not honored the request. + cts.Token.ThrowIfCancellationRequested(); } catch (OperationCanceledException) when (cts.IsCancellationRequested) { From 52e1b003dcb4eadf907b8f86215e27b7ad9a4c7c Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Fri, 7 Jul 2023 02:43:03 +0200 Subject: [PATCH 14/23] FIX: AsyncService.State may be Stopped instead of Disposed after disposing --- src/Louis/Threading/AsyncService.cs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index 92a4e57..9f23db5 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -140,7 +140,7 @@ public Task StartAsync(CancellationToken cancellationToken) /// if the service was running and has been requested to stop; /// if the service was not running. /// - public bool Stop() => StopCore(false); + public bool Stop() => StopCore(); /// /// Asynchronously stops an asynchronous service and waits for it to complete. @@ -153,7 +153,7 @@ public Task StartAsync(CancellationToken cancellationToken) /// public async Task StopAsync() { - if (!StopCore(false)) + if (!StopCore()) { return false; } @@ -206,7 +206,7 @@ public async ValueTask DisposeAsync() return; } - hadStarted = StopCore(true); + hadStarted = StopCore(); } if (hadStarted) @@ -214,6 +214,7 @@ public async ValueTask DisposeAsync() await WaitUntilStoppedAsync().ConfigureAwait(false); } + State = AsyncServiceState.Disposed; await DisposeResourcesAsync().ConfigureAwait(false); _stoppedTokenSource.Dispose(); } @@ -348,7 +349,7 @@ private static void AggregateAndThrowIfNeeded(Exception? exception1, Exception? private static void ThrowMultipleExceptions(params Exception[] innerExceptions) => throw new AggregateException(innerExceptions); - private bool StopCore(bool disposing) + private bool StopCore() { lock (_stateSyncRoot) { @@ -357,22 +358,16 @@ private bool StopCore(bool disposing) case < AsyncServiceState.Starting: _ = _startedCompletionSource.TrySetResult(false); _ = _stoppedCompletionSource.TrySetResult(true); - UnsafeSetState(disposing ? AsyncServiceState.Disposed : AsyncServiceState.Stopped); + UnsafeSetState(AsyncServiceState.Stopped); return false; case AsyncServiceState.Disposed: return false; case > AsyncServiceState.Running: - if (disposing) - { - UnsafeSetState(AsyncServiceState.Disposed); - } - return true; default: - UnsafeSetState(disposing ? AsyncServiceState.Disposed : AsyncServiceState.Stopping); _stoppedTokenSource.Cancel(); return true; } @@ -433,10 +428,10 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella if (!setupCompleted) { + State = AsyncServiceState.Stopped; _startedCompletionSource.SetResult(false); _stoppedCompletionSource.SetResult(true); _doneTokenSource.Cancel(); - State = AsyncServiceState.Stopped; // Only propagate exceptions if there is a caller to propagate to. if (exception is not null && !runInBackground) From d167c4056b41a6f00ea2f0054e47ae794123b4ce Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Fri, 7 Jul 2023 07:21:48 +0200 Subject: [PATCH 15/23] FIX: AsyncService never disposes _doneToken --- src/Louis/Threading/AsyncService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index 9f23db5..c93bbf0 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -217,6 +217,7 @@ public async ValueTask DisposeAsync() State = AsyncServiceState.Disposed; await DisposeResourcesAsync().ConfigureAwait(false); _stoppedTokenSource.Dispose(); + _doneTokenSource.Dispose(); } /// From 6ad546214b7a376cb10985a1ecbad14cbae3035f Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Fri, 7 Jul 2023 07:26:03 +0200 Subject: [PATCH 16/23] Improve stop request logic in AsyncService --- src/Louis/Threading/AsyncService.cs | 71 ++++++++++++++++------------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index c93bbf0..737726b 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -35,7 +35,7 @@ namespace Louis.Threading; /// public abstract class AsyncService : IAsyncDisposable, IDisposable { - private readonly CancellationTokenSource _stoppedTokenSource = new(); + private readonly CancellationTokenSource _stopTokenSource = new(); private readonly CancellationTokenSource _doneTokenSource = new(); private readonly TaskCompletionSource _startedCompletionSource = new(); private readonly TaskCompletionSource _stoppedCompletionSource = new(); @@ -140,7 +140,41 @@ public Task StartAsync(CancellationToken cancellationToken) /// if the service was running and has been requested to stop; /// if the service was not running. /// - public bool Stop() => StopCore(); + public bool Stop() + { + lock (_stateSyncRoot) + { + return StopCore(); + } + + bool StopCore() + { + switch (_state) + { + case AsyncServiceState.Created: + _ = _startedCompletionSource.TrySetResult(false); + _ = _stoppedCompletionSource.TrySetResult(true); + UnsafeSetState(AsyncServiceState.Stopped); + return false; + + case AsyncServiceState.Starting: + case AsyncServiceState.Running: + _stopTokenSource.Cancel(); + return true; + + case AsyncServiceState.Stopping: + case AsyncServiceState.Stopped: + return true; + + case AsyncServiceState.Disposed: + return false; + + default: + SelfCheck.Fail($"Unexpected async service state ({_state})"); + return false; + } + } + } /// /// Asynchronously stops an asynchronous service and waits for it to complete. @@ -153,7 +187,7 @@ public Task StartAsync(CancellationToken cancellationToken) /// public async Task StopAsync() { - if (!StopCore()) + if (!Stop()) { return false; } @@ -206,7 +240,7 @@ public async ValueTask DisposeAsync() return; } - hadStarted = StopCore(); + hadStarted = Stop(); } if (hadStarted) @@ -216,7 +250,7 @@ public async ValueTask DisposeAsync() State = AsyncServiceState.Disposed; await DisposeResourcesAsync().ConfigureAwait(false); - _stoppedTokenSource.Dispose(); + _stopTokenSource.Dispose(); _doneTokenSource.Dispose(); } @@ -350,31 +384,6 @@ private static void AggregateAndThrowIfNeeded(Exception? exception1, Exception? private static void ThrowMultipleExceptions(params Exception[] innerExceptions) => throw new AggregateException(innerExceptions); - private bool StopCore() - { - lock (_stateSyncRoot) - { - switch (_state) - { - case < AsyncServiceState.Starting: - _ = _startedCompletionSource.TrySetResult(false); - _ = _stoppedCompletionSource.TrySetResult(true); - UnsafeSetState(AsyncServiceState.Stopped); - return false; - - case AsyncServiceState.Disposed: - return false; - - case > AsyncServiceState.Running: - return true; - - default: - _stoppedTokenSource.Cancel(); - return true; - } - } - } - private async Task RunAsyncCore(bool runInBackground, CancellationToken cancellationToken) { lock (_stateSyncRoot) @@ -400,7 +409,7 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella await Task.Yield(); } - using var cts = CancellationTokenSource.CreateLinkedTokenSource(_stoppedTokenSource.Token, cancellationToken); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(_stopTokenSource.Token, cancellationToken); var setupCompleted = false; Exception? exception = null; try From fc46bc5dfc441394e4ce43df958979819c31e228 Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Fri, 7 Jul 2023 07:26:51 +0200 Subject: [PATCH 17/23] FIX: AsyncService.Stop returns true for a stopped service --- src/Louis/Threading/AsyncService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index 737726b..c7d1def 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -163,9 +163,9 @@ bool StopCore() return true; case AsyncServiceState.Stopping: - case AsyncServiceState.Stopped: return true; + case AsyncServiceState.Stopped: case AsyncServiceState.Disposed: return false; From 1905545ef9d17892784b33e1833d28a4a933b3c2 Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Fri, 7 Jul 2023 07:27:49 +0200 Subject: [PATCH 18/23] ADD: Logging hook for stop request in AsyncService --- src/Louis.Hosting/AsyncHostedService.cs | 10 ++++++++++ src/Louis.Hosting/Internal/EventIds.cs | 1 + src/Louis.Hosting/PublicAPI.Unshipped.txt | 1 + src/Louis/PublicAPI.Unshipped.txt | 1 + src/Louis/Threading/AsyncService.cs | 17 ++++++++++++++++- 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Louis.Hosting/AsyncHostedService.cs b/src/Louis.Hosting/AsyncHostedService.cs index 75259b5..797efa9 100644 --- a/src/Louis.Hosting/AsyncHostedService.cs +++ b/src/Louis.Hosting/AsyncHostedService.cs @@ -86,4 +86,14 @@ async Task IHostedService.StartAsync(CancellationToken cancellationToken) level: LogLevel.Error, message: "Service teardown phase failed")] protected sealed override partial void LogTeardownFailed(Exception exception); + + /// + protected sealed override void LogStopRequested(AsyncServiceState previousState, AsyncServiceState currentState, bool result) + => LogStopRequestedCore(previousState, result ? "running" : "not running"); + + [LoggerMessage( + eventId: EventIds.AsyncHostedService.StopRequested, + level: LogLevel.Information, + message: "Stop requested while service {running} ({previousState})")] + private partial void LogStopRequestedCore(AsyncServiceState previousState, string running); } diff --git a/src/Louis.Hosting/Internal/EventIds.cs b/src/Louis.Hosting/Internal/EventIds.cs index 3050267..a7126c0 100644 --- a/src/Louis.Hosting/Internal/EventIds.cs +++ b/src/Louis.Hosting/Internal/EventIds.cs @@ -13,5 +13,6 @@ public static class AsyncHostedService public const int ExecuteCanceled = 3; public const int ExecuteFailed = 4; public const int TeardownFailed = 5; + public const int StopRequested = 6; } } diff --git a/src/Louis.Hosting/PublicAPI.Unshipped.txt b/src/Louis.Hosting/PublicAPI.Unshipped.txt index e76f850..c1bf164 100644 --- a/src/Louis.Hosting/PublicAPI.Unshipped.txt +++ b/src/Louis.Hosting/PublicAPI.Unshipped.txt @@ -6,4 +6,5 @@ override sealed Louis.Hosting.AsyncHostedService.LogExecuteFailed(System.Excepti override sealed Louis.Hosting.AsyncHostedService.LogSetupCanceled() -> void override sealed Louis.Hosting.AsyncHostedService.LogSetupFailed(System.Exception! exception) -> void override sealed Louis.Hosting.AsyncHostedService.LogStateChanged(Louis.Threading.AsyncServiceState oldState, Louis.Threading.AsyncServiceState newState) -> void +override sealed Louis.Hosting.AsyncHostedService.LogStopRequested(Louis.Threading.AsyncServiceState previousState, Louis.Threading.AsyncServiceState currentState, bool result) -> void override sealed Louis.Hosting.AsyncHostedService.LogTeardownFailed(System.Exception! exception) -> void diff --git a/src/Louis/PublicAPI.Unshipped.txt b/src/Louis/PublicAPI.Unshipped.txt index 38ae902..955fac4 100644 --- a/src/Louis/PublicAPI.Unshipped.txt +++ b/src/Louis/PublicAPI.Unshipped.txt @@ -194,6 +194,7 @@ virtual Louis.Threading.AsyncService.LogExecuteFailed(System.Exception! exceptio virtual Louis.Threading.AsyncService.LogSetupCanceled() -> void virtual Louis.Threading.AsyncService.LogSetupFailed(System.Exception! exception) -> void virtual Louis.Threading.AsyncService.LogStateChanged(Louis.Threading.AsyncServiceState oldState, Louis.Threading.AsyncServiceState newState) -> void +virtual Louis.Threading.AsyncService.LogStopRequested(Louis.Threading.AsyncServiceState previousState, Louis.Threading.AsyncServiceState currentState, bool result) -> void virtual Louis.Threading.AsyncService.LogTeardownFailed(System.Exception! exception) -> void virtual Louis.Threading.AsyncService.SetupAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask virtual Louis.Threading.AsyncService.TeardownAsync() -> System.Threading.Tasks.ValueTask diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index c7d1def..3f35b22 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -144,7 +144,10 @@ public bool Stop() { lock (_stateSyncRoot) { - return StopCore(); + var previousState = _state; + var result = StopCore(); + LogStopRequested(previousState, _state, result); + return result; } bool StopCore() @@ -357,6 +360,18 @@ protected virtual void LogTeardownFailed(Exception exception) { } + /// + /// Called upon requesting that a service stops. + /// This method must return as early as possible, must not throw, and should be only used for logging purposes. + /// + /// The value of the property before the stop request. + /// The value of the property after the stop request. + /// if the service was running when requested to stop; + /// otherwise. + protected virtual void LogStopRequested(AsyncServiceState previousState, AsyncServiceState currentState, bool result) + { + } + [DoesNotReturn] private static void ThrowOnObjectDisposed(string message) => throw new ObjectDisposedException(message); From 2aa2167c0afb1f48af9896a5e8b1ccb922c17ea7 Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Fri, 7 Jul 2023 08:50:07 +0200 Subject: [PATCH 19/23] In AsyncSource, avoid throwing when getting DoneToken even if disposed. --- src/Louis/Threading/AsyncService.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index 3f35b22..ee974e7 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -35,6 +35,8 @@ namespace Louis.Threading; /// public abstract class AsyncService : IAsyncDisposable, IDisposable { + private static readonly CancellationTokenSource AlreadyDoneTokenSource = new(); + private readonly CancellationTokenSource _stopTokenSource = new(); private readonly CancellationTokenSource _doneTokenSource = new(); private readonly TaskCompletionSource _startedCompletionSource = new(); @@ -44,6 +46,11 @@ public abstract class AsyncService : IAsyncDisposable, IDisposable private InterlockedFlag _disposed; private AsyncServiceState _state = AsyncServiceState.Created; + static AsyncService() + { + AlreadyDoneTokenSource.Cancel(); + } + /// /// Initializes a new instance of the class. /// @@ -77,7 +84,7 @@ private set /// Gets a that is canceled as soon as the service has finished executing /// (either successfully or with an exception) or has failed starting. /// - public CancellationToken DoneToken => _doneTokenSource.Token; + public CancellationToken DoneToken => (_disposed.Value ? AlreadyDoneTokenSource : _doneTokenSource).Token; /// /// Asynchronously runs an asynchronous service. From 4bb054ac13014ede0d155f30c6fe9e7647971496 Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Fri, 7 Jul 2023 08:51:53 +0200 Subject: [PATCH 20/23] Make sure that as soon as an AsyncService begins disposing, its State always returns Disposed. --- src/Louis/Threading/AsyncService.cs | 127 ++++++++++++++-------------- 1 file changed, 64 insertions(+), 63 deletions(-) diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index ee974e7..22b1114 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -2,7 +2,6 @@ // See the LICENSE file in the project root for full license information. using System; -using System.Diagnostics.CodeAnalysis; using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; @@ -29,7 +28,7 @@ namespace Louis.Threading; /// as soon as execution of the service stops, just before the teardown phase starts; /// you can synchronize other tasks with your service by calling /// and ; -/// you can stop your service and let it complete in the background by calling , +/// you can stop your service and let it complete in the background by calling , /// or stop and wait for completion by calling . /// /// @@ -68,7 +67,7 @@ public AsyncServiceState State { lock (_stateSyncRoot) { - return _state; + return _disposed.Value ? AsyncServiceState.Disposed : _state; } } private set @@ -147,44 +146,7 @@ public Task StartAsync(CancellationToken cancellationToken) /// if the service was running and has been requested to stop; /// if the service was not running. /// - public bool Stop() - { - lock (_stateSyncRoot) - { - var previousState = _state; - var result = StopCore(); - LogStopRequested(previousState, _state, result); - return result; - } - - bool StopCore() - { - switch (_state) - { - case AsyncServiceState.Created: - _ = _startedCompletionSource.TrySetResult(false); - _ = _stoppedCompletionSource.TrySetResult(true); - UnsafeSetState(AsyncServiceState.Stopped); - return false; - - case AsyncServiceState.Starting: - case AsyncServiceState.Running: - _stopTokenSource.Cancel(); - return true; - - case AsyncServiceState.Stopping: - return true; - - case AsyncServiceState.Stopped: - case AsyncServiceState.Disposed: - return false; - - default: - SelfCheck.Fail($"Unexpected async service state ({_state})"); - return false; - } - } - } + public bool Stop() => Stop(true); /// /// Asynchronously stops an asynchronous service and waits for it to complete. @@ -197,7 +159,7 @@ bool StopCore() /// public async Task StopAsync() { - if (!Stop()) + if (!Stop(true)) { return false; } @@ -250,7 +212,7 @@ public async ValueTask DisposeAsync() return; } - hadStarted = Stop(); + hadStarted = Stop(false); } if (hadStarted) @@ -258,7 +220,6 @@ public async ValueTask DisposeAsync() await WaitUntilStoppedAsync().ConfigureAwait(false); } - State = AsyncServiceState.Disposed; await DisposeResourcesAsync().ConfigureAwait(false); _stopTokenSource.Dispose(); _doneTokenSource.Dispose(); @@ -379,17 +340,13 @@ protected virtual void LogStopRequested(AsyncServiceState previousState, AsyncSe { } - [DoesNotReturn] - private static void ThrowOnObjectDisposed(string message) - => throw new ObjectDisposedException(message); - private static void AggregateAndThrowIfNeeded(Exception? exception1, Exception? exception2) { if (exception1 is not null) { if (exception2 is not null) { - ThrowMultipleExceptions(exception1, exception2); + throw new AggregateException(exception1, exception2); } else { @@ -402,26 +359,17 @@ private static void AggregateAndThrowIfNeeded(Exception? exception1, Exception? } } - [DoesNotReturn] - private static void ThrowMultipleExceptions(params Exception[] innerExceptions) - => throw new AggregateException(innerExceptions); - private async Task RunAsyncCore(bool runInBackground, CancellationToken cancellationToken) { lock (_stateSyncRoot) { - switch (_state) + EnsureNotDisposed(); + if (_state != AsyncServiceState.Created) { - case AsyncServiceState.Created: - UnsafeSetState(AsyncServiceState.Starting); - break; - case AsyncServiceState.Disposed: - ThrowOnObjectDisposed("Trying to run a disposed async service."); - break; - default: - ThrowHelper.ThrowInvalidOperationException("An async service cannot be started more than once."); - return; + ThrowHelper.ThrowInvalidOperationException("An async service cannot be started more than once."); } + + UnsafeSetState(AsyncServiceState.Starting); } // Return immediately when called from Start or StartAsync; @@ -516,6 +464,51 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella AggregateAndThrowIfNeeded(exception, teardownException); } + private bool Stop(bool checkDisposed) + { + lock (_stateSyncRoot) + { + if (checkDisposed && _disposed.Value) + { + LogStopRequested(AsyncServiceState.Disposed, AsyncServiceState.Disposed, false); + return false; + } + + var previousState = _state; + var result = StopCore(); + LogStopRequested(previousState, _state, result); + return result; + } + + bool StopCore() + { + switch (_state) + { + case AsyncServiceState.Created: + _ = _startedCompletionSource.TrySetResult(false); + _ = _stoppedCompletionSource.TrySetResult(true); + UnsafeSetState(AsyncServiceState.Stopped); + return false; + + case AsyncServiceState.Starting: + case AsyncServiceState.Running: + _stopTokenSource.Cancel(); + return true; + + case AsyncServiceState.Stopping: + return true; + + case AsyncServiceState.Stopped: + case AsyncServiceState.Disposed: + return false; + + default: + SelfCheck.Fail($"Unexpected async service state ({_state})"); + return false; + } + } + } + private void UnsafeSetState(AsyncServiceState value) { if (value == _state) @@ -527,4 +520,12 @@ private void UnsafeSetState(AsyncServiceState value) _state = value; LogStateChanged(oldState, _state); } + + private void EnsureNotDisposed() + { + if (_disposed.Value) + { + throw new ObjectDisposedException(GetType().Name); + } + } } From eb2a6a1715c5ff7b634a63528416d561ce4a872d Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Fri, 7 Jul 2023 09:56:54 +0200 Subject: [PATCH 21/23] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58bc076..c779134 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### New features +- Added class `Louis.Threading.AsyncService`: a complete revamp of the old `AsyncWorker` class that was present in the very first alpha version of L.o.U.I.S., this class simplifies the implementation and use of long-running background tasks. +- Added package `Louis.Hosting` with an `AsyncHostedService` class, that extends `AsyncService` with logging and implements the [`IHostedService`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice) interface for integration in ASP.NET applications, as well as any application based on the [`generic host`](https://learn.microsoft.com/en-us/dotnet/core/extensions/generic-host). + ### Changes to existing features ### Bugs fixed in this release From cd15569b25fee3423b2233b3862112fabf998bae Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Fri, 7 Jul 2023 10:22:16 +0200 Subject: [PATCH 22/23] Reduce the complexity of AsyncService.RunAsyncCore --- src/Louis/Threading/AsyncService.cs | 124 ++++++++++++++++------------ 1 file changed, 69 insertions(+), 55 deletions(-) diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index 22b1114..49e1d66 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -380,32 +380,7 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella } using var cts = CancellationTokenSource.CreateLinkedTokenSource(_stopTokenSource.Token, cancellationToken); - var setupCompleted = false; - Exception? exception = null; - try - { - // Check the cancellation token first, in case it is already canceled. - cts.Token.ThrowIfCancellationRequested(); - - // Perform start actions. - await SetupAsync(cts.Token).ConfigureAwait(false); - - // Check the cancellation token again, in case cancellation has been requested - // but SetupAsync has not honored the request. - cts.Token.ThrowIfCancellationRequested(); - - setupCompleted = true; - } - catch (OperationCanceledException) when (cts.IsCancellationRequested) - { - LogSetupCanceled(); - } - catch (Exception e) when (!e.IsCriticalError()) - { - LogSetupFailed(e); - exception = e; - } - + var (setupCompleted, exception) = await RunSetupAsync(cts.Token).ConfigureAwait(false); if (!setupCompleted) { State = AsyncServiceState.Stopped; @@ -424,44 +399,83 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella State = AsyncServiceState.Running; _startedCompletionSource.SetResult(true); - try - { - // Check the cancellation token first, in case it is already canceled. - cts.Token.ThrowIfCancellationRequested(); + exception = await RunExecuteAsync(cts.Token).ConfigureAwait(false); + _doneTokenSource.Cancel(); - // Execute the service. - await ExecuteAsync(cts.Token).ConfigureAwait(false); + State = AsyncServiceState.Stopping; + var teardownException = await RunTeardownAsync().ConfigureAwait(false); + State = AsyncServiceState.Stopped; + _stoppedCompletionSource.SetResult(true); + AggregateAndThrowIfNeeded(exception, teardownException); - // Check the cancellation token again, in case cancellation has been requested - // but ExecuteAsync has not honored the request. - cts.Token.ThrowIfCancellationRequested(); - } - catch (OperationCanceledException) when (cts.IsCancellationRequested) - { - LogExecuteCanceled(); - } - catch (Exception e) when (!e.IsCriticalError()) + async Task<(bool Completed, Exception? FailureException)> RunSetupAsync(CancellationToken cancellationToken) { - LogExecuteFailed(e); - exception = e; + try + { + // Check the cancellation token first, in case it is already canceled. + cancellationToken.ThrowIfCancellationRequested(); + + // Perform start actions. + await SetupAsync(cancellationToken).ConfigureAwait(false); + + // Check the cancellation token again, in case cancellation has been requested + // but SetupAsync has not honored the request. + cancellationToken.ThrowIfCancellationRequested(); + + return (true, null); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + LogSetupCanceled(); + return (false, null); + } + catch (Exception e) when (!e.IsCriticalError()) + { + LogSetupFailed(e); + return (false, e); + } } - Exception? teardownException = null; - _doneTokenSource.Cancel(); - State = AsyncServiceState.Stopping; - try + async Task RunExecuteAsync(CancellationToken cancellationToken) { - await TeardownAsync().ConfigureAwait(false); + try + { + // Check the cancellation token first, in case it is already canceled. + cancellationToken.ThrowIfCancellationRequested(); + + // Execute the service. + await ExecuteAsync(cancellationToken).ConfigureAwait(false); + + // Check the cancellation token again, in case cancellation has been requested + // but ExecuteAsync has not honored the request. + cancellationToken.ThrowIfCancellationRequested(); + return null; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + LogExecuteCanceled(); + return null; + } + catch (Exception e) when (!e.IsCriticalError()) + { + LogExecuteFailed(e); + return e; + } } - catch (Exception e) when (!e.IsCriticalError()) + + async Task RunTeardownAsync() { - LogTeardownFailed(e); - teardownException = e; + try + { + await TeardownAsync().ConfigureAwait(false); + return null; + } + catch (Exception e) when (!e.IsCriticalError()) + { + LogTeardownFailed(e); + return e; + } } - - State = AsyncServiceState.Stopped; - _stoppedCompletionSource.SetResult(true); - AggregateAndThrowIfNeeded(exception, teardownException); } private bool Stop(bool checkDisposed) From cc91ccc660159c83967d620189f0c40ba4848c52 Mon Sep 17 00:00:00 2001 From: Tenacom developer Date: Fri, 7 Jul 2023 10:26:15 +0200 Subject: [PATCH 23/23] Ditch nested methods in AsyncService.RunAsyncCore to keep CodeFactor happy. RunAsyncCore was still flagged as "Complex method". --- src/Louis/Threading/AsyncService.cs | 110 ++++++++++++++-------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/src/Louis/Threading/AsyncService.cs b/src/Louis/Threading/AsyncService.cs index 49e1d66..f3065f3 100644 --- a/src/Louis/Threading/AsyncService.cs +++ b/src/Louis/Threading/AsyncService.cs @@ -407,74 +407,74 @@ private async Task RunAsyncCore(bool runInBackground, CancellationToken cancella State = AsyncServiceState.Stopped; _stoppedCompletionSource.SetResult(true); AggregateAndThrowIfNeeded(exception, teardownException); + } - async Task<(bool Completed, Exception? FailureException)> RunSetupAsync(CancellationToken cancellationToken) + private async Task<(bool Completed, Exception? FailureException)> RunSetupAsync(CancellationToken cancellationToken) + { + try { - try - { - // Check the cancellation token first, in case it is already canceled. - cancellationToken.ThrowIfCancellationRequested(); + // Check the cancellation token first, in case it is already canceled. + cancellationToken.ThrowIfCancellationRequested(); - // Perform start actions. - await SetupAsync(cancellationToken).ConfigureAwait(false); + // Perform start actions. + await SetupAsync(cancellationToken).ConfigureAwait(false); - // Check the cancellation token again, in case cancellation has been requested - // but SetupAsync has not honored the request. - cancellationToken.ThrowIfCancellationRequested(); + // Check the cancellation token again, in case cancellation has been requested + // but SetupAsync has not honored the request. + cancellationToken.ThrowIfCancellationRequested(); - return (true, null); - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - LogSetupCanceled(); - return (false, null); - } - catch (Exception e) when (!e.IsCriticalError()) - { - LogSetupFailed(e); - return (false, e); - } + return (true, null); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + LogSetupCanceled(); + return (false, null); + } + catch (Exception e) when (!e.IsCriticalError()) + { + LogSetupFailed(e); + return (false, e); + } + } - async Task RunExecuteAsync(CancellationToken cancellationToken) + private async Task RunExecuteAsync(CancellationToken cancellationToken) + { + try { - try - { - // Check the cancellation token first, in case it is already canceled. - cancellationToken.ThrowIfCancellationRequested(); + // Check the cancellation token first, in case it is already canceled. + cancellationToken.ThrowIfCancellationRequested(); - // Execute the service. - await ExecuteAsync(cancellationToken).ConfigureAwait(false); + // Execute the service. + await ExecuteAsync(cancellationToken).ConfigureAwait(false); - // Check the cancellation token again, in case cancellation has been requested - // but ExecuteAsync has not honored the request. - cancellationToken.ThrowIfCancellationRequested(); - return null; - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - LogExecuteCanceled(); - return null; - } - catch (Exception e) when (!e.IsCriticalError()) - { - LogExecuteFailed(e); - return e; - } + // Check the cancellation token again, in case cancellation has been requested + // but ExecuteAsync has not honored the request. + cancellationToken.ThrowIfCancellationRequested(); + return null; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + LogExecuteCanceled(); + return null; + } + catch (Exception e) when (!e.IsCriticalError()) + { + LogExecuteFailed(e); + return e; } + } - async Task RunTeardownAsync() + private async Task RunTeardownAsync() + { + try { - try - { - await TeardownAsync().ConfigureAwait(false); - return null; - } - catch (Exception e) when (!e.IsCriticalError()) - { - LogTeardownFailed(e); - return e; - } + await TeardownAsync().ConfigureAwait(false); + return null; + } + catch (Exception e) when (!e.IsCriticalError()) + { + LogTeardownFailed(e); + return e; } }