Skip to content

Commit

Permalink
Add base class for implementing IBackgroundServiceExceptionHandler
Browse files Browse the repository at this point in the history
  • Loading branch information
Putta Khunchalee committed Dec 2, 2019
1 parent d9e5e87 commit 73f933b
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,6 @@ public void Constructor_WhenSuccess_ShouldInitializeProperties()
this.subject.Logger.Should().NotBeNull();
}

[Fact]
public void RunAsync_WithNullService_ShouldThrow()
{
var ex = new Exception();

this.subject.Invoking(s => s.RunAsync(null, ex, CancellationToken.None))
.Should().ThrowExactly<ArgumentNullException>()
.And.ParamName.Should().Be("service");
}

[Fact]
public void RunAsync_WithNullException_ShouldThrow()
{
this.subject.Invoking(s => s.RunAsync(typeof(string), null, CancellationToken.None))
.Should().ThrowExactly<ArgumentNullException>()
.And.ParamName.Should().Be("exception");
}

[Fact]
public async Task RunAsync_WithValidArguments_ShouldInvokeLoggerAndCollector()
{
Expand All @@ -66,7 +48,7 @@ public async Task RunAsync_WithValidArguments_ShouldInvokeLoggerAndCollector()
using (var cancellationToken = new CancellationTokenSource())
{
// Act.
await this.subject.RunAsync(service, ex, cancellationToken.Token);
await ((IBackgroundServiceExceptionHandler)this.subject).RunAsync(service, ex, cancellationToken.Token);

// Assert.
this.logger.Verify(
Expand Down
29 changes: 15 additions & 14 deletions src/Ztm.Hosting.AspNetCore/BackgroundServiceExceptionHandler.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using System;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

namespace Ztm.Hosting.AspNetCore
{
public class BackgroundServiceExceptionHandler : IBackgroundServiceExceptionHandler
public class BackgroundServiceExceptionHandler : Ztm.Hosting.BackgroundServiceExceptionHandler
{
readonly Collection<IBackgroundServiceExceptionHandler> inners;

public BackgroundServiceExceptionHandler(ILoggerFactory loggerFactory)
{
if (loggerFactory == null)
Expand All @@ -16,28 +19,26 @@ public BackgroundServiceExceptionHandler(ILoggerFactory loggerFactory)

Logger = new BackgroundServiceErrorLogger(loggerFactory.CreateLogger<BackgroundServiceErrorLogger>());
Collector = new BackgroundServiceErrorCollector();

this.inners = new Collection<IBackgroundServiceExceptionHandler>()
{
Logger,
Collector
};
}

public BackgroundServiceErrorCollector Collector { get; }

public BackgroundServiceErrorLogger Logger { get; }

public async Task RunAsync(Type service, Exception exception, CancellationToken cancellationToken)
protected override async Task RunAsync(Type service, Exception exception, CancellationToken cancellationToken)
{
if (service == null)
{
throw new ArgumentNullException(nameof(service));
}

if (exception == null)
{
throw new ArgumentNullException(nameof(exception));
}

// We cannot do IApplicationLifetime.StopApplication() here due to there is a race condition if background
// task error too early. So we use another approach.
await Logger.RunAsync(service, exception, cancellationToken);
await Collector.RunAsync(service, exception, CancellationToken.None);
foreach (var handler in this.inners)
{
await handler.RunAsync(service, exception, CancellationToken.None);
}
}
}
}
25 changes: 7 additions & 18 deletions src/Ztm.Hosting.Tests/BackgroundServiceErrorCollectorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,12 @@ public BackgroundServiceErrorCollectorTests()
this.subject = new BackgroundServiceErrorCollector();
}

[Fact]
public void RunAsync_WithNullService_ShouldThrow()
{
this.subject.Invoking(s => s.RunAsync(null, new Exception(), CancellationToken.None))
.Should().ThrowExactly<ArgumentNullException>()
.And.ParamName.Should().Be("service");
}

[Fact]
public void RunAsync_WithNullException_ShouldThrow()
{
this.subject.Invoking(s => s.RunAsync(typeof(FakeBackgroundService), null, CancellationToken.None))
.Should().ThrowExactly<ArgumentNullException>()
.And.ParamName.Should().Be("exception");
}

[Fact]
public async Task RunAsync_WithValidArguments_ShouldSuccess()
{
var ex = new Exception();

await this.subject.RunAsync(typeof(FakeBackgroundService), ex, CancellationToken.None);
await RunAsync(typeof(FakeBackgroundService), ex, CancellationToken.None);

this.subject.Should().ContainSingle(e => e.Service == typeof(FakeBackgroundService) && e.Exception == ex);
}
Expand All @@ -59,7 +43,7 @@ public async Task GetEnumerator_WhenIterate_ShouldFoundTheSameAddedItems(int ite
{
var error = new BackgroundServiceError(typeof(FakeBackgroundService), new Exception());

await this.subject.RunAsync(typeof(FakeBackgroundService), new Exception(), CancellationToken.None);
await RunAsync(typeof(FakeBackgroundService), new Exception(), CancellationToken.None);
generated.Add(error);
}

Expand All @@ -75,5 +59,10 @@ public async Task GetEnumerator_WhenIterate_ShouldFoundTheSameAddedItems(int ite
// Assert.
collected.Should().BeEquivalentTo(generated);
}

Task RunAsync(Type service, Exception exception, CancellationToken cancellationToken)
{
return ((IBackgroundServiceExceptionHandler)this.subject).RunAsync(service, exception, cancellationToken);
}
}
}
24 changes: 4 additions & 20 deletions src/Ztm.Hosting.Tests/BackgroundServiceErrorLoggerTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Internal;
using Moq;
Expand All @@ -26,30 +25,15 @@ public void Constructor_WithNullLogger_ShouldThrow()
Assert.Throws<ArgumentNullException>("logger", () => new BackgroundServiceErrorLogger(null));
}

[Fact]
public void RunAsync_WithNullService_ShouldThrow()
{
var ex = new Exception();

this.subject.Invoking(s => s.RunAsync(null, ex, CancellationToken.None))
.Should().ThrowExactly<ArgumentNullException>()
.And.ParamName.Should().Be("service");
}

[Fact]
public void RunAsync_WithNullException_ShouldThrow()
{
this.subject.Invoking(s => s.RunAsync(typeof(FakeBackgroundService), null, CancellationToken.None))
.Should().ThrowExactly<ArgumentNullException>()
.And.ParamName.Should().Be("exception");
}

[Fact]
public async Task RunAsync_WithValidArguments_ShouldInvokeLogger()
{
var ex = new Exception();

await this.subject.RunAsync(typeof(FakeBackgroundService), ex, CancellationToken.None);
await ((IBackgroundServiceExceptionHandler)this.subject).RunAsync(
typeof(FakeBackgroundService),
ex,
CancellationToken.None);

this.logger.Verify(
l => l.Log(
Expand Down
58 changes: 58 additions & 0 deletions src/Ztm.Hosting.Tests/BackgroundServiceExceptionHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Xunit;
using Ztm.Testing;

namespace Ztm.Hosting.Tests
{
public sealed class BackgroundServiceExceptionHandlerTests
{
readonly FakeBackgroundServiceExceptionHandler subject;

public BackgroundServiceExceptionHandlerTests()
{
this.subject = new FakeBackgroundServiceExceptionHandler();
}

[Fact]
public async Task RunAsync_WithNullService_ShouldThrow()
{
var exception = new Exception();

await Assert.ThrowsAsync<ArgumentNullException>(
"service",
() => this.subject.InvokeRunAsync(null, exception, CancellationToken.None)
);
}

[Fact]
public async Task RunAsync_WithNullException_ShouldThrow()
{
await Assert.ThrowsAsync<ArgumentNullException>(
"exception",
() => this.subject.InvokeRunAsync(typeof(FakeBackgroundService), null, CancellationToken.None)
);
}

[Fact]
public Task RunAsync_WithValidArgs_ShouldInvokeProtectedRunAsync()
{
return AsynchronousTesting.WithCancellationTokenAsync(async cancellationToken =>
{
// Arrange.
var exception = new Exception();

// Act.
await this.subject.InvokeRunAsync(typeof(FakeBackgroundService), exception, cancellationToken);

// Assert.
this.subject.StubbedRunAsync.Verify(
f => f(typeof(FakeBackgroundService), exception, cancellationToken),
Times.Once()
);
});
}
}
}
22 changes: 22 additions & 0 deletions src/Ztm.Hosting.Tests/FakeBackgroundServiceExceptionHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Moq;

namespace Ztm.Hosting.Tests
{
sealed class FakeBackgroundServiceExceptionHandler : BackgroundServiceExceptionHandler
{
public FakeBackgroundServiceExceptionHandler()
{
StubbedRunAsync = new Mock<Func<Type, Exception, CancellationToken, Task>>();
}

public Mock<Func<Type, Exception, CancellationToken, Task>> StubbedRunAsync { get; }

protected override Task RunAsync(Type service, Exception exception, CancellationToken cancellationToken)
{
return StubbedRunAsync.Object(service, exception, cancellationToken);
}
}
}
32 changes: 12 additions & 20 deletions src/Ztm.Hosting/BackgroundServiceErrorCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

namespace Ztm.Hosting
{
public sealed class BackgroundServiceErrorCollector : IBackgroundServiceErrorCollector
public sealed class BackgroundServiceErrorCollector :
BackgroundServiceExceptionHandler,
IBackgroundServiceErrorCollector
{
readonly Collection<BackgroundServiceError> errors;

Expand All @@ -16,35 +18,25 @@ public BackgroundServiceErrorCollector()
this.errors = new Collection<BackgroundServiceError>();
}

public Task RunAsync(Type service, Exception exception, CancellationToken cancellationToken)
public IEnumerator<BackgroundServiceError> GetEnumerator()
{
if (service == null)
{
throw new ArgumentNullException(nameof(service));
}

if (exception == null)
{
throw new ArgumentNullException(nameof(exception));
}

lock (this.errors)
{
this.errors.Add(new BackgroundServiceError(service, exception));
foreach (var item in this.errors)
{
yield return item;
}
}

return Task.CompletedTask;
}

public IEnumerator<BackgroundServiceError> GetEnumerator()
protected override Task RunAsync(Type service, Exception exception, CancellationToken cancellationToken)
{
lock (this.errors)
{
foreach (var item in this.errors)
{
yield return item;
}
this.errors.Add(new BackgroundServiceError(service, exception));
}

return Task.CompletedTask;
}

IEnumerator IEnumerable.GetEnumerator()
Expand Down
14 changes: 2 additions & 12 deletions src/Ztm.Hosting/BackgroundServiceErrorLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace Ztm.Hosting
{
public sealed class BackgroundServiceErrorLogger : IBackgroundServiceExceptionHandler
public sealed class BackgroundServiceErrorLogger : BackgroundServiceExceptionHandler
{
readonly ILogger logger;

Expand All @@ -19,18 +19,8 @@ public BackgroundServiceErrorLogger(ILogger<BackgroundServiceErrorLogger> logger
this.logger = logger;
}

public Task RunAsync(Type service, Exception exception, CancellationToken cancellationToken)
protected override Task RunAsync(Type service, Exception exception, CancellationToken cancellationToken)
{
if (service == null)
{
throw new ArgumentNullException(nameof(service));
}

if (exception == null)
{
throw new ArgumentNullException(nameof(exception));
}

this.logger.LogCritical(exception, "Fatal error occurred in {Service}.", service);

return Task.CompletedTask;
Expand Down
38 changes: 38 additions & 0 deletions src/Ztm.Hosting/BackgroundServiceExceptionHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Ztm.Hosting
{
public abstract class BackgroundServiceExceptionHandler : IBackgroundServiceExceptionHandler
{
protected BackgroundServiceExceptionHandler()
{
}

public Task InvokeRunAsync(Type service, Exception exception, CancellationToken cancellationToken)
{
return ((IBackgroundServiceExceptionHandler)this).RunAsync(service, exception, cancellationToken);
}

protected abstract Task RunAsync(Type service, Exception exception, CancellationToken cancellationToken);

Task IBackgroundServiceExceptionHandler.RunAsync(
Type service,
Exception exception,
CancellationToken cancellationToken)
{
if (service == null)
{
throw new ArgumentNullException(nameof(service));
}

if (exception == null)
{
throw new ArgumentNullException(nameof(exception));
}

return RunAsync(service, exception, cancellationToken);
}
}
}

0 comments on commit 73f933b

Please sign in to comment.