Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement asynchronous background services #61

Merged
merged 23 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7ae47b2
Add AsyncService class. Same as old AsyncWorker for now.
ric15ni Jul 4, 2023
41b1ef4
Fix XML docs in AsyncService.
ric15ni Jul 4, 2023
029e77c
Make overriding OnStartServiceAsync and OnStopServiceAsync optional.
ric15ni Jul 4, 2023
37a9b9d
BRK: Rename core overridable methods in AsyncService, ditching the On…
ric15ni Jul 4, 2023
8aa6bb3
BRK: Simplify stopping and disposing logic in AsyncService
ric15ni Jul 6, 2023
effbff1
Partially refactor AsyncService:
ric15ni Jul 6, 2023
1b86faf
ADD: AsyncService.DoneToken to monitor execution from a dependent ser…
ric15ni Jul 6, 2023
97f2568
ADD: Logging hooks in AsyncService.
ric15ni Jul 6, 2023
f97f98a
BRK: Rename logging hooks in AsyncService to make their purpose clear
ric15ni Jul 6, 2023
6895d9b
Add Louis.Hosting project
ric15ni Jul 6, 2023
2428460
ADD: AsyncHostedService, a subclass of AsyncService that implements I…
ric15ni Jul 6, 2023
82f61c5
Improve readability of some null checks in AsyncService
ric15ni Jul 7, 2023
3374dfc
Do not call AsyncService.SetupAsync and ExecuteAsync if cancellation …
ric15ni Jul 7, 2023
52e1b00
FIX: AsyncService.State may be Stopped instead of Disposed after disp…
ric15ni Jul 7, 2023
d167c40
FIX: AsyncService never disposes _doneToken
ric15ni Jul 7, 2023
6ad5462
Improve stop request logic in AsyncService
ric15ni Jul 7, 2023
fc46bc5
FIX: AsyncService.Stop returns true for a stopped service
ric15ni Jul 7, 2023
1905545
ADD: Logging hook for stop request in AsyncService
ric15ni Jul 7, 2023
2aa2167
In AsyncSource, avoid throwing when getting DoneToken even if disposed.
ric15ni Jul 7, 2023
4bb054a
Make sure that as soon as an AsyncService begins disposing, its State…
ric15ni Jul 7, 2023
eb2a6a1
Update changelog
ric15ni Jul 7, 2023
cd15569
Reduce the complexity of AsyncService.RunAsyncCore
ric15ni Jul 7, 2023
cc91ccc
Ditch nested methods in AsyncService.RunAsyncCore to keep CodeFactor …
ric15ni Jul 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<ItemGroup>
<PackageVersion Include="CommunityToolkit.Diagnostics" Version="8.2.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageVersion Include="PolyKit" Version="2.0.30" />
<PackageVersion Include="System.Memory" Version="4.5.5" />
Expand Down
7 changes: 7 additions & 0 deletions Louis.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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}
Expand Down
99 changes: 99 additions & 0 deletions src/Louis.Hosting/AsyncHostedService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// 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;

/// <summary>
/// Subclasses <see cref="AsyncService"/>, providing support for logging and implementing <see cref="IHostedService"/>.
/// </summary>
public abstract partial class AsyncHostedService : AsyncService, IHostedService
{
private readonly ILogger _logger;

/// <summary>
/// Initializes a new instance of the <see cref="AsyncHostedService"/> class.
/// </summary>
/// <param name="logger">An <see cref="ILogger"/> to use for logging.</param>
protected AsyncHostedService(ILogger logger)
{
Guard.IsNotNull(logger);
_logger = logger;
}

/// <inheritdoc/>
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.");
}

/// <inheritdoc/>
Task IHostedService.StopAsync(CancellationToken cancellationToken) => Task.WhenAny(StopAsync(), Task.Delay(Timeout.Infinite, cancellationToken));

/// <inheritdoc/>
[LoggerMessage(
eventId: EventIds.AsyncHostedService.StateChanged,
level: LogLevel.Trace,
message: "Service state {oldState} -> {newState}")]
protected sealed override partial void LogStateChanged(AsyncServiceState oldState, AsyncServiceState newState);

/// <inheritdoc/>
[LoggerMessage(
eventId: EventIds.AsyncHostedService.SetupCanceled,
level: LogLevel.Warning,
message: "Service execution was canceled during setup phase")]
protected sealed override partial void LogSetupCanceled();

/// <inheritdoc/>
[LoggerMessage(
eventId: EventIds.AsyncHostedService.SetupFailed,
level: LogLevel.Error,
message: "Service setup phase failed")]
protected sealed override partial void LogSetupFailed(Exception exception);

/// <inheritdoc/>
[LoggerMessage(
eventId: EventIds.AsyncHostedService.ExecuteCanceled,
level: LogLevel.Warning,
message: "Service execution was canceled")]
protected sealed override partial void LogExecuteCanceled();

/// <inheritdoc/>
[LoggerMessage(
eventId: EventIds.AsyncHostedService.ExecuteFailed,
level: LogLevel.Error,
message: "Service execution failed")]
protected sealed override partial void LogExecuteFailed(Exception exception);

/// <inheritdoc/>
[LoggerMessage(
eventId: EventIds.AsyncHostedService.TeardownFailed,
level: LogLevel.Error,
message: "Service teardown phase failed")]
protected sealed override partial void LogTeardownFailed(Exception exception);

/// <inheritdoc/>
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);
}
18 changes: 18 additions & 0 deletions src/Louis.Hosting/Internal/EventIds.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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;
public const int StopRequested = 6;
}
}
19 changes: 19 additions & 0 deletions src/Louis.Hosting/Louis.Hosting.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>Integration of L.o.U.I.S. with Microsoft.Extensions.Hosting.</Description>
<TargetFrameworks>netstandard2.0;netstandard2.1;net462;net6.0;net7.0</TargetFrameworks>
<UseTfmSpecificPublicApiFiles>false</UseTfmSpecificPublicApiFiles>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Louis\Louis.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="CommunityToolkit.Diagnostics" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>

</Project>
10 changes: 10 additions & 0 deletions src/Louis.Hosting/NuGet-README.md
Original file line number Diff line number Diff line change
@@ -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)

1 change: 1 addition & 0 deletions src/Louis.Hosting/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
10 changes: 10 additions & 0 deletions src/Louis.Hosting/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#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.LogStopRequested(Louis.Threading.AsyncServiceState previousState, Louis.Threading.AsyncServiceState currentState, bool result) -> void
override sealed Louis.Hosting.AsyncHostedService.LogTeardownFailed(System.Exception! exception) -> void
31 changes: 31 additions & 0 deletions src/Louis/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#nullable enable
abstract Louis.Threading.AsyncService.ExecuteAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
const Louis.Diagnostics.ExceptionHelper.NullText = "<null>" -> string!
const Louis.Diagnostics.ExceptionHelper.ToStringEmptyText = "<empty!>" -> string!
const Louis.Diagnostics.ExceptionHelper.ToStringNullText = "<null!>" -> string!
Expand Down Expand Up @@ -34,6 +35,26 @@ 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.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<bool>!
Louis.Threading.AsyncService.State.get -> Louis.Threading.AsyncServiceState
Louis.Threading.AsyncService.Stop() -> bool
Louis.Threading.AsyncService.StopAsync() -> System.Threading.Tasks.Task<bool>!
Louis.Threading.AsyncService.WaitUntilStartedAsync() -> System.Threading.Tasks.Task<bool>!
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
Expand Down Expand Up @@ -167,3 +188,13 @@ 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<System.Threading.Tasks.ValueTask>! 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.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.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
Loading