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

Update ContextReference & DefaultFromBodyConversionFeature to remove invocation cancellation token usage #2894

Merged
merged 18 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from 13 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
2 changes: 2 additions & 0 deletions eng/ci/templates/jobs/run-integration-tests-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,5 @@ jobs:
projects: |
**\E2ETests.csproj
**\SdkE2ETests.csproj
env:
DOTNET_VERSION: $(dotnetVersion)
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

### Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore <version>

- <entry>
- Update `ContextReference` to no longer use a given invocation's cancellation token (#2894)

### Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.Analyzers <version>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,14 @@

namespace Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore
{
internal class ContextReference : IDisposable
internal class ContextReference
{
private readonly TaskCompletionSource<bool> _functionStartTask = new();
private readonly TaskCompletionSource<bool> _functionCompletionTask = new();

private TaskCompletionSource<HttpContext> _httpContextValueSource = new();
private TaskCompletionSource<FunctionContext> _functionContextValueSource = new();

private CancellationToken _token;
private CancellationTokenRegistration _tokenRegistration;

public ContextReference(string invocationId)
{
InvocationId = invocationId;
Expand All @@ -32,18 +29,6 @@ public ContextReference(string invocationId)

public TaskCompletionSource<FunctionContext> FunctionContextValueSource { get => _functionContextValueSource; set => _functionContextValueSource = value; }

internal void SetCancellationToken(CancellationToken token)
{
_token = token;
_tokenRegistration = _token.Register(() =>
{
_functionStartTask.TrySetCanceled();
_functionCompletionTask.TrySetCanceled();
_functionContextValueSource.TrySetCanceled();
_httpContextValueSource.TrySetCanceled();
});
}

internal Task InvokeFunctionAsync()
{
_functionStartTask.SetResult(true);
Expand All @@ -59,27 +44,12 @@ internal void CompleteFunction()

if (_httpContextValueSource.Task.IsCompleted)
{
if (_httpContextValueSource.Task.IsCanceled || _token.IsCancellationRequested)
{
_functionCompletionTask.TrySetCanceled();
}
else
{
_functionCompletionTask.TrySetResult(true);
}
_functionCompletionTask.TrySetResult(true);
}
else
{
// we should never reach here b/c the class that calls this needs httpContextValueSource to complete to reach this method
}
}

public void Dispose()
{
if (_tokenRegistration != default)
{
_tokenRegistration.Dispose();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
Expand Down Expand Up @@ -44,7 +44,6 @@ public async Task<FunctionContext> SetHttpContextAsync(string invocationId, Http
public async Task<HttpContext> SetFunctionContextAsync(string invocationId, FunctionContext context)
{
var contextRef = _contextReferenceList.GetOrAdd(invocationId, static id => new ContextReference(id));
contextRef.SetCancellationToken(context.CancellationToken);
contextRef.FunctionContextValueSource.SetResult(context);

_logger.FunctionContextSet(invocationId);
Expand Down Expand Up @@ -85,7 +84,6 @@ public void CompleteFunctionInvocation(string invocationId)
if (_contextReferenceList.TryRemove(invocationId, out var contextRef))
{
contextRef.CompleteFunction();
contextRef.Dispose();
}
else
{
Expand Down
1 change: 1 addition & 0 deletions extensions/Worker.Extensions.Http/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
### Microsoft.Azure.Functions.Worker.Extensions.Http <version>

- The 'FromBody' converter now utilizes `DeserializeAsync` for deserializing JSON content from the request body, enhancing support for asynchronous deserialization. (#2901)
- Update `DefaultFromBodyConversionFeature` to no longer use a given invocation's cancellation token (#2894)
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ internal class DefaultFromBodyConversionFeature : IFromBodyConversionFeature
_ when HasJsonContentType(requestData) =>
await (requestData.FunctionContext.InstanceServices.GetService<IOptions<WorkerOptions>>()?.Value?.Serializer
?? throw new InvalidOperationException("A serializer is not configured for the worker."))
.DeserializeAsync(requestData.Body, targetType, context.CancellationToken),
.DeserializeAsync(requestData.Body, targetType, CancellationToken.None),
_ => throw new InvalidOperationException($"The type '{targetType}' is not supported by the '{nameof(DefaultFromBodyConversionFeature)}'.")
};

Expand Down
2 changes: 1 addition & 1 deletion setup-e2e-tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ if ($SkipBuildOnPack -eq $true)
$AdditionalPackArgs += "--no-build"
}

.\tools\devpack.ps1 -E2E -AdditionalPackArgs $AdditionalPackArgs
.\tools\devpack.ps1 -DotnetVersion $DotnetVersion -E2E -AdditionalPackArgs $AdditionalPackArgs

if ($SkipStorageEmulator -And $SkipCosmosDBEmulator)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;

namespace Microsoft.Azure.Functions.Worker.E2EApp
{
public class CancellationHttpFunctions(ILogger<CancellationHttpFunctions> logger)
{
private readonly ILogger<CancellationHttpFunctions> _logger = logger;

[Function(nameof(HttpWithCancellationTokenNotUsed))]
public async Task<IActionResult> HttpWithCancellationTokenNotUsed(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req)
{
_logger.LogInformation("HttpWithCancellationTokenNotUsed processed a request.");

await SimulateWork(CancellationToken.None);

return new OkObjectResult("Processing completed successfully.");
}

[Function(nameof(HttpWithCancellationTokenIgnored))]
public async Task<IActionResult> HttpWithCancellationTokenIgnored(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req,
CancellationToken cancellationToken)
{
_logger.LogInformation("HttpWithCancellationTokenIgnored processed a request.");

await SimulateWork(cancellationToken);

return new OkObjectResult("Processing completed successfully.");
}

[Function(nameof(HttpWithCancellationTokenHandled))]
public async Task<IActionResult> HttpWithCancellationTokenHandled(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
CancellationToken cancellationToken)
{
_logger.LogInformation("HttpWithCancellationTokenHandled processed a request.");

try
{
await SimulateWork(cancellationToken);

return new OkObjectResult("Processing completed successfully.");
}
catch (OperationCanceledException)
{
_logger.LogWarning("Request was cancelled.");

// Take precautions like noting how far along you are with processing the batch
await Task.Delay(1000);

return new ObjectResult(new { statusCode = StatusCodes.Status499ClientClosedRequest, message = "Request was cancelled." });
}
}

private async Task SimulateWork(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting work...");

for (int i = 0; i < 5; i++)
{
// Simulate work
await Task.Delay(1000, cancellationToken);
_logger.LogWarning($"Work iteration {i + 1} completed.");
}

_logger.LogInformation("Work completed.");
}
}
}
32 changes: 32 additions & 0 deletions test/E2ETests/E2EApps/E2EAspNetCoreApp/E2EAspNetCoreApp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Microsoft.Azure.Functions.Worker.E2EAspNetCoreApp</AssemblyName>
<RootNamespace>Microsoft.Azure.Functions.Worker.E2EAspNetCoreApp</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\extensions\Worker.Extensions.Http\src\Worker.Extensions.Http.csproj" />
<ProjectReference Include="..\..\..\..\extensions\Worker.Extensions.Http.AspNetCore\src\Worker.Extensions.Http.AspNetCore.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.0" />
<PackageReference Condition="$(TestBuild) != 'true'" Include="Microsoft.Azure.Functions.Worker" Version="2.0.0" />
</ItemGroup>
</Project>
7 changes: 7 additions & 0 deletions test/E2ETests/E2EApps/E2EAspNetCoreApp/NuGet.Config
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="AzureFunctionsTempStaging" value="https://azfunc.pkgs.visualstudio.com/e6a70c92-4128-439f-8012-382fe78d6396/_packaging/AzureFunctionsTempStaging/nuget/v3/index.json" />
</packageSources>
</configuration>
brettsam marked this conversation as resolved.
Show resolved Hide resolved
9 changes: 9 additions & 0 deletions test/E2ETests/E2EApps/E2EAspNetCoreApp/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;

var host = new HostBuilder()
.ConfigureFunctionsWebApplication()
.Build();

host.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"profiles": {
"dni_net8_cancellation": {
"commandName": "Project",
"commandLineArgs": "--port 7097",
"launchBrowser": false
}
}
}
12 changes: 12 additions & 0 deletions test/E2ETests/E2EApps/E2EAspNetCoreApp/host.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
},
"enableLiveMetricsFilters": true
}
}
}
67 changes: 67 additions & 0 deletions test/E2ETests/E2ETests/AspNetCore/CancellationEndToEndTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Functions.Tests;
using Microsoft.Azure.Functions.Tests.E2ETests;
using Xunit;
using Xunit.Abstractions;

namespace Microsoft.Azure.Functions.Worker.E2ETests.AspNetCore
{
public class CancellationEndToEndTests : IClassFixture<CancellationEndToEndTests.TestFixture>
{
private readonly TestFixture _fixture;

public CancellationEndToEndTests(TestFixture fixture, ITestOutputHelper testOutputHelper)
{
_fixture = fixture;
_fixture.TestLogs.UseTestLogger(testOutputHelper);
}

[IgnoreOnNetFxTestRunTheory]
[InlineData("HttpWithCancellationTokenNotUsed", "Work completed.", "Succeeded")]
[InlineData("HttpWithCancellationTokenIgnored", "TaskCanceledException: A task was canceled", "Failed")]
[InlineData("HttpWithCancellationTokenHandled", "Request was cancelled.", "Succeeded")]
public async Task HttpTriggerFunctions_WithCancellationToken_BehaveAsExpected(string functionName, string expectedMessage, string invocationResult)
liliankasem marked this conversation as resolved.
Show resolved Hide resolved
{
using var cts = new CancellationTokenSource();

try
{
var task = HttpHelpers.InvokeHttpTrigger(functionName, cancellationToken: cts.Token);

await Task.Delay(3000);
cts.Cancel();

var response = await task;
}
catch (Exception)
liliankasem marked this conversation as resolved.
Show resolved Hide resolved
{
IEnumerable<string> logs = null;
await TestUtility.RetryAsync(() =>
{
logs = _fixture.TestLogs.CoreToolsLogs.Where(p => p.Contains($"Executed 'Functions.{functionName}'"));
liliankasem marked this conversation as resolved.
Show resolved Hide resolved

return Task.FromResult(logs.Count() >= 1);
});

Assert.Contains(_fixture.TestLogs.CoreToolsLogs, log => log.Contains(expectedMessage, StringComparison.OrdinalIgnoreCase));

// TODO: 2/3 of the test invocations will fail until the host with the ForwarderProxy fix is released - uncomment this line when the fix is released
// Assert.Contains(_fixture.TestLogs.CoreToolsLogs, log => log.Contains($"'Functions.{functionName}' ({invocationResult}", StringComparison.OrdinalIgnoreCase));
}
}

public class TestFixture : FunctionAppFixture
{
public TestFixture(IMessageSink messageSink) : base(messageSink, Constants.TestAppNames.E2EAspNetCoreApp)
{
}
}
}
}
6 changes: 6 additions & 0 deletions test/E2ETests/E2ETests/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,5 +98,11 @@ public static class Tables
public const string TablesConnectionStringSetting = EmulatorConnectionString;
public const string TableName = "TestTable";
}

public static class TestAppNames
{
public const string E2EApp = "E2EApp";
public const string E2EAspNetCoreApp = "E2EAspNetCoreApp";
}
}
}
4 changes: 2 additions & 2 deletions test/E2ETests/E2ETests/Fixtures/FixtureHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ namespace Microsoft.Azure.Functions.Tests.E2ETests
{
public static class FixtureHelpers
{
public static Process GetFuncHostProcess(bool enableAuth = false)
public static Process GetFuncHostProcess(bool enableAuth = false, string testAppName = null)
{
var funcProcess = new Process();
var rootDir = Path.GetFullPath(@"../../../../../..");
var e2eAppBinPath = Path.Combine(rootDir, @"test/E2ETests/E2EApps/E2EApp/bin");
var e2eAppBinPath = Path.Combine(rootDir, "test", "E2ETests", "E2EApps", testAppName, "bin");
string e2eHostJson = Directory.GetFiles(e2eAppBinPath, "host.json", SearchOption.AllDirectories).FirstOrDefault();

if (e2eHostJson == null)
Expand Down
Loading
Loading