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

Introduce BackgroundService utility class in StartupHook #6655

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
using Microsoft.Diagnostics.Tools.Monitor.ParameterCapturing;
using Microsoft.Diagnostics.Tools.Monitor.Profiler;
using Microsoft.Diagnostics.Tools.Monitor.StartupHook;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using BackgroundService = Microsoft.Extensions.Hosting.BackgroundService;

namespace Microsoft.Diagnostics.Monitoring.HostingStartup.ParameterCapturing
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Diagnostics.Monitoring.StartupHook
{
internal abstract class BackgroundService : IDisposable
{
private readonly CancellationTokenSource _cts = new();
private long _disposedState;

public Task? ExecutingTask { get; private set; }

public void Start()
{
ExecutingTask = Task.Run(async () =>
{
await ExecuteAsync(_cts.Token).ConfigureAwait(false);
}, _cts.Token);
}

public void Stop()
{
SafeCancel();

try
{
ExecutingTask?.Wait(TimeSpan.FromSeconds(1));
}
catch
{
// ignore
}
}

public virtual void Dispose()
{
if (!DisposableHelper.CanDispose(ref _disposedState))
return;

SafeCancel();
_cts.Dispose();
}

private void SafeCancel()
{
try
{
_cts.Cancel();
}
catch (AggregateException)
{
// Ignore all exceptions thrown by registered callbacks on the associated CancellationToken.
}
}

protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Diagnostics.Monitoring.TestCommon;
using System;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.Diagnostics.Monitoring.StartupHook
{
[TargetFrameworkMonikerTrait(TargetFrameworkMonikerExtensions.CurrentTargetFrameworkMoniker)]
public sealed class BackgroundServiceTests
{
[Fact]
public void ConstructionWorks()
{
using BackgroundService _ = new MockBackgroundService();
}

[Fact]
public async Task StartBackgroundTask()
clguiman marked this conversation as resolved.
Show resolved Hide resolved
clguiman marked this conversation as resolved.
Show resolved Hide resolved
{
// Arrange
using MockBackgroundService service = new MockBackgroundService();

// Act
service.Start();

// Assert
await service.BackgroundTaskStarted.Task;
}

[Fact]
public async Task StopTriggersCancellation()
{
// Arrange
using MockBackgroundService service = new MockBackgroundService(async (CancellationToken stoppingToken) =>
{
await Task.Delay(Timeout.Infinite, stoppingToken);
});

// Act
service.Start();
await service.BackgroundTaskStarted.Task;
service.Stop();

// Assert
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => service.ExecutingTask!);
clguiman marked this conversation as resolved.
Show resolved Hide resolved
}

[Fact]
public async Task StopWaitsForTheBackgroundTask()
{
// Arrange
object lockObj = new();
bool stopCompleted = false;
bool taskCompleted = false;
TaskCompletionSource backgroundTaskCompletion = new();
clguiman marked this conversation as resolved.
Show resolved Hide resolved
TaskCompletionSource beforeStopCompletion = new();

MockBackgroundService service = new MockBackgroundService(async _ =>
{
await backgroundTaskCompletion.Task;
lock (lockObj)
{
Assert.False(stopCompleted, "Stop completed before the background task.");
taskCompleted = true;
}
});

// Act
service.Start();
await service.BackgroundTaskStarted.Task;

Task stopTask = Task.Run(async () =>
{
await Task.Yield();
beforeStopCompletion.SetResult();
service.Stop();

lock (lockObj)
{
Assert.True(taskCompleted, "Stop completed before the background task.");
stopCompleted = true;
}
});

await beforeStopCompletion.Task;
// Wait a bit to ensure Stop() is waiting for the background task to complete
await Task.Delay(TimeSpan.FromMilliseconds(100));

backgroundTaskCompletion.SetResult();

await stopTask;

// Assert
Assert.False(service.ExecutingTask?.IsFaulted);
}

[Fact]
public async Task BackgroundTaskExceptionIsCaptured()
{
// Arrange
MockBackgroundService service = new MockBackgroundService(async _ =>
{
await Task.Yield();
throw new NotImplementedException();
});

// Act
service.Start();
await service.BackgroundTaskStarted.Task;

service.Stop();
service.Dispose();

// Assert
await Assert.ThrowsAsync<NotImplementedException>(() => service.ExecutingTask!);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Diagnostics.Monitoring.StartupHook
{
internal sealed class MockBackgroundService : BackgroundService, IDisposable
{
private readonly Func<CancellationToken, Task> _backgroundFunc;

public MockBackgroundService()
{
_backgroundFunc = _ => Task.CompletedTask;
}

public MockBackgroundService(Func<CancellationToken, Task> backgroundFunc)
{
_backgroundFunc = backgroundFunc;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
BackgroundTaskStarted.SetResult();

await _backgroundFunc(stoppingToken);
}

public override void Dispose()
{
base.Dispose();
}

public TaskCompletionSource BackgroundTaskStarted { get; } = new();
}
}
Loading