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 1 commit
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,55 @@
// 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 Task? _executeTask;
private long _disposedState;

public void Start()
{
_executeTask = Task.Run(async () =>
{
try
clguiman marked this conversation as resolved.
Show resolved Hide resolved
{
await ExecuteAsync(_cts.Token);
clguiman marked this conversation as resolved.
Show resolved Hide resolved
}
catch (OperationCanceledException)
{
// Ignore
}
catch (Exception ex)
{
BackgroundTaskException = ex;
}
});
clguiman marked this conversation as resolved.
Show resolved Hide resolved
}

public void Stop()
{
_cts.Cancel();
}

public Exception? BackgroundTaskException { get; private set; }
clguiman marked this conversation as resolved.
Show resolved Hide resolved

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

_executeTask?.Wait();
clguiman marked this conversation as resolved.
Show resolved Hide resolved
clguiman marked this conversation as resolved.
Show resolved Hide resolved
clguiman marked this conversation as resolved.
Show resolved Hide resolved
_executeTask = null;

_cts.Dispose();
}

protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// 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.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
TaskCompletionSource backgroundTaskCompletion = new();
using MockBackgroundService service = new MockBackgroundService(backgroundTaskCompletion.Task);

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

// Assert
Assert.False(service.BackgroundTaskWasCancelled);

service.Stop();
backgroundTaskCompletion.SetResult();
await service.BackgroundTaskEnded.Task;

Assert.True(service.BackgroundTaskWasCancelled);
Assert.Null(service.BackgroundTaskException);
}

[Fact]
public async Task DisposeWaitsForTheBackgroundTask()
{
// Arrange
object lockObj = new();
int callOrderMarker = 1;
TaskCompletionSource backgroundTaskCompletion = new();
clguiman marked this conversation as resolved.
Show resolved Hide resolved

// If Dispose() completes first, callOrderMarker will be 10
// Otherwise, callOrderMarker will be 20
void OnDisposeCompleted()
{
lock (lockObj)
{
callOrderMarker *= 10;
}
}

async Task BackgroundWork()
{
await backgroundTaskCompletion.Task;
lock (lockObj)
{
callOrderMarker += 1;
}
}

MockBackgroundService service = new MockBackgroundService(BackgroundWork(), OnDisposeCompleted);

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

Task disposeTask = Task.Run(service.Dispose);

await service.DisposeStarted.Task;
await Task.Delay(100); // Ensure that Dispose is waiting for the background task to complete
backgroundTaskCompletion.SetResult();

await disposeTask;

// Assert
Assert.Null(service.BackgroundTaskException);
Assert.Equal(20, callOrderMarker);
}

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

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

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

// Assert
Assert.IsType<NotImplementedException>(service.BackgroundTaskException);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// 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 Task _backgroundTask;
private readonly Action _postDisposeAction;

public MockBackgroundService()
{
_backgroundTask = Task.CompletedTask;
_postDisposeAction = () => { };
}

public MockBackgroundService(Task backgroundTaskInput)
{
_backgroundTask = backgroundTaskInput;
_postDisposeAction = () => { };
}

public MockBackgroundService(Task backgroundTaskInput, Action postDisposeAction)
{
_backgroundTask = backgroundTaskInput;
_postDisposeAction = postDisposeAction;
}

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

await _backgroundTask;

if (stoppingToken.IsCancellationRequested)
{
BackgroundTaskWasCancelled = true;
}

BackgroundTaskEnded.SetResult();
}

public override void Dispose()
{
DisposeStarted.SetResult();

base.Dispose();

_postDisposeAction();
}

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

public TaskCompletionSource BackgroundTaskEnded { get; } = new();

public TaskCompletionSource DisposeStarted { get; } = new();

public bool BackgroundTaskWasCancelled { get; private set; }
}
}
Loading