Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -41,9 +41,12 @@ public virtual Task StartAsync(CancellationToken cancellationToken)
{
// Create linked token to allow cancelling executing task from provided token
_stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
CancellationToken stoppingToken = _stoppingCts.Token;

// Execute all of ExecuteAsync asynchronously, and store the task we're executing so that we can wait for it later.
_executeTask = Task.Run(() => ExecuteAsync(_stoppingCts.Token), _stoppingCts.Token);
_executeTask = cancellationToken.IsCancellationRequested
? Task.FromCanceled(cancellationToken)
: Task.Run(() => ExecuteAsync(stoppingToken), CancellationToken.None);

// Always return a completed task. Any result from ExecuteAsync will be handled by the Host.
return Task.CompletedTask;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.RemoteExecutor;
using Xunit;

namespace Microsoft.Extensions.Hosting.Tests
{
public class BackgroundServiceTests
{
public static bool IsThreadingAndRemoteExecutorSupported =>
PlatformDetection.IsMultithreadingSupported && RemoteExecutor.IsSupported;

[Fact]
public void StartReturnsCompletedTask()
{
Expand All @@ -29,11 +33,14 @@ public void StartReturnsCompletedTask()
public async Task StartCancelledThrowsTaskCanceledException()
{
var ct = new CancellationToken(true);
var service = new WaitForCancelledTokenService();
var service = new TrackingBackgroundService();

await service.StartAsync(ct);
Task startTask = service.StartAsync(ct);

Assert.True(startTask.IsCompleted);
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => service.ExecuteTask);
Assert.True(service.ExecuteTask.IsCanceled);
Assert.False(service.ExecuteInvocation.IsCompleted);
}

[Fact]
Expand Down Expand Up @@ -146,6 +153,86 @@ public async Task StartSynchronousExecuteShouldBeCancelable()
await service.WaitForEndExecuteTask;
}

[ConditionalTheory(typeof(BackgroundServiceTests), nameof(IsThreadingAndRemoteExecutorSupported))]
[InlineData(false)]
[InlineData(true)]
public void ExecuteAsyncRunsWhenImmediatelyStoppedOrDisposed(bool dispose)
{
var options = new RemoteInvokeOptions();
options.StartInfo.EnvironmentVariables["DOTNET_ThreadPool_UseWindowsThreadPool"] = "0";

using var _ = RemoteExecutor.Invoke((string disposeString) =>
{
ThreadPool.GetMinThreads(out int originalMinWorkerThreads, out int originalMinCompletionPortThreads);
ThreadPool.GetMaxThreads(out int originalMaxWorkerThreads, out int originalMaxCompletionPortThreads);
Assert.True(ThreadPool.SetMinThreads(1, originalMinCompletionPortThreads));
Assert.True(ThreadPool.SetMaxThreads(1, originalMaxCompletionPortThreads));

using var blockerEntered = new ManualResetEventSlim();
using var releaseBlocker = new ManualResetEventSlim();

try
{
ThreadPool.QueueUserWorkItem(_ =>
{
blockerEntered.Set();
releaseBlocker.Wait();
});
Assert.True(blockerEntered.Wait(RemoteExecutor.FailWaitTimeoutMilliseconds));

int startThreadId = Environment.CurrentManagedThreadId;
var service = new TrackingBackgroundService();
service.StartAsync(CancellationToken.None).GetAwaiter().GetResult();

Task stopTask;
if (bool.Parse(disposeString))
{
service.Dispose();
stopTask = service.ExecuteTask;
}
else
{
stopTask = service.StopAsync(CancellationToken.None);
}

releaseBlocker.Set();
stopTask.GetAwaiter().GetResult();

(int invocationCount, int threadId, bool isThreadPoolThread, bool isCancellationRequested) =
service.ExecuteInvocation.GetAwaiter().GetResult();
Assert.Equal(1, invocationCount);
Assert.NotEqual(startThreadId, threadId);
Assert.True(isThreadPoolThread);
Assert.True(isCancellationRequested);
}
finally
{
releaseBlocker.Set();
ThreadPool.SetMaxThreads(originalMaxWorkerThreads, originalMaxCompletionPortThreads);
ThreadPool.SetMinThreads(originalMinWorkerThreads, originalMinCompletionPortThreads);
}
}, dispose.ToString(), options);
}

private sealed class TrackingBackgroundService : BackgroundService
{
private readonly TaskCompletionSource<(int InvocationCount, int ThreadId, bool IsThreadPoolThread, bool IsCancellationRequested)> _executeInvocation =
new(TaskCreationOptions.RunContinuationsAsynchronously);
private int _invocationCount;

public Task<(int InvocationCount, int ThreadId, bool IsThreadPoolThread, bool IsCancellationRequested)> ExecuteInvocation => _executeInvocation.Task;

protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
_executeInvocation.SetResult((
Interlocked.Increment(ref _invocationCount),
Environment.CurrentManagedThreadId,
Thread.CurrentThread.IsThreadPoolThread,
stoppingToken.IsCancellationRequested));
return Task.CompletedTask;
}
}

private class WaitForCancelledTokenService : BackgroundService
{
private TaskCompletionSource<object> _waitForExecuteTask = new TaskCompletionSource<object>();
Expand Down
Loading