Handle InvocationCancel message and signal cancellation for in-flight invocations (#972)
This commit is contained in:
Родитель
90e6f922f9
Коммит
3b4ae8c222
|
@ -5,4 +5,5 @@
|
||||||
|
|
||||||
- Add Retry Policy Attributes (#977, #971)
|
- Add Retry Policy Attributes (#977, #971)
|
||||||
- Bug fix - GetOutputBindings returns incorrect data when OutputBindingData is not set (#983)
|
- Bug fix - GetOutputBindings returns incorrect data when OutputBindingData is not set (#983)
|
||||||
- Worker version and environment information returned in init call (WorkerMetadata)
|
- Worker version and environment information returned in init call (#1022)
|
||||||
|
- Handle InvocationCancel message and signal cancellation for in-flight invocations (#972)
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
// 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.Net;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Azure.Functions.Worker;
|
||||||
|
using Microsoft.Azure.Functions.Worker.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace FunctionApp
|
||||||
|
{
|
||||||
|
public static class HttpTriggerWithCancellation
|
||||||
|
{
|
||||||
|
[Function(nameof(HttpTriggerWithCancellation))]
|
||||||
|
public static async Task<HttpResponseData> Run(
|
||||||
|
[HttpTrigger(AuthorizationLevel.Anonymous,"get", "post", Route = null)]
|
||||||
|
HttpRequestData req,
|
||||||
|
FunctionContext executionContext,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var logger = executionContext.GetLogger("FunctionApp.HttpTriggerWithCancellation");
|
||||||
|
logger.LogInformation("HttpTriggerWithCancellation function triggered");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||||
|
response.WriteString($"Hello world!");
|
||||||
|
|
||||||
|
await Task.Delay(5000, cancellationToken);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Function invocation cancelled");
|
||||||
|
|
||||||
|
var response = req.CreateResponse(HttpStatusCode.ServiceUnavailable);
|
||||||
|
response.WriteString("Invocation cancelled");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
// 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.
|
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Azure.Functions.Worker;
|
using Microsoft.Azure.Functions.Worker;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
@ -13,7 +16,7 @@ namespace FunctionApp
|
||||||
static async Task Main(string[] args)
|
static async Task Main(string[] args)
|
||||||
{
|
{
|
||||||
// #if DEBUG
|
// #if DEBUG
|
||||||
// Debugger.Launch();
|
// Debugger.Launch();
|
||||||
// #endif
|
// #endif
|
||||||
//<docsnippet_startup>
|
//<docsnippet_startup>
|
||||||
var host = new HostBuilder()
|
var host = new HostBuilder()
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
@ -12,15 +13,15 @@ namespace Microsoft.Azure.Functions.Worker
|
||||||
{
|
{
|
||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
private readonly FunctionInvocation _invocation;
|
private readonly FunctionInvocation _invocation;
|
||||||
|
|
||||||
private IServiceScope? _instanceServicesScope;
|
private IServiceScope? _instanceServicesScope;
|
||||||
private IServiceProvider? _instanceServices;
|
private IServiceProvider? _instanceServices;
|
||||||
private BindingContext? _bindingContext;
|
private BindingContext? _bindingContext;
|
||||||
|
|
||||||
public DefaultFunctionContext(IServiceScopeFactory serviceScopeFactory, IInvocationFeatures features)
|
public DefaultFunctionContext(IServiceScopeFactory serviceScopeFactory, IInvocationFeatures features, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
_serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
|
_serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
|
||||||
Features = features ?? throw new ArgumentNullException(nameof(features));
|
Features = features ?? throw new ArgumentNullException(nameof(features));
|
||||||
|
CancellationToken = cancellationToken;
|
||||||
|
|
||||||
_invocation = features.Get<FunctionInvocation>() ?? throw new InvalidOperationException($"The '{nameof(FunctionInvocation)}' feature is required.");
|
_invocation = features.Get<FunctionInvocation>() ?? throw new InvalidOperationException($"The '{nameof(FunctionInvocation)}' feature is required.");
|
||||||
FunctionDefinition = features.Get<FunctionDefinition>() ?? throw new InvalidOperationException($"The {nameof(Worker.FunctionDefinition)} feature is required.");
|
FunctionDefinition = features.Get<FunctionDefinition>() ?? throw new InvalidOperationException($"The {nameof(Worker.FunctionDefinition)} feature is required.");
|
||||||
|
@ -36,6 +37,8 @@ namespace Microsoft.Azure.Functions.Worker
|
||||||
|
|
||||||
public override IInvocationFeatures Features { get; }
|
public override IInvocationFeatures Features { get; }
|
||||||
|
|
||||||
|
public override CancellationToken CancellationToken { get; }
|
||||||
|
|
||||||
public override IServiceProvider InstanceServices
|
public override IServiceProvider InstanceServices
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Microsoft.Azure.Functions.Worker.Pipeline
|
namespace Microsoft.Azure.Functions.Worker.Pipeline
|
||||||
|
@ -15,9 +16,9 @@ namespace Microsoft.Azure.Functions.Worker.Pipeline
|
||||||
_serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
|
_serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
|
||||||
}
|
}
|
||||||
|
|
||||||
public FunctionContext Create(IInvocationFeatures invocationFeatures)
|
public FunctionContext Create(IInvocationFeatures invocationFeatures, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return new DefaultFunctionContext(_serviceScopeFactory, invocationFeatures);
|
return new DefaultFunctionContext(_serviceScopeFactory, invocationFeatures, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace Microsoft.Azure.Functions.Worker
|
namespace Microsoft.Azure.Functions.Worker
|
||||||
{
|
{
|
||||||
|
@ -59,5 +60,10 @@ namespace Microsoft.Azure.Functions.Worker
|
||||||
/// Gets a collection containing the features supported by this context.
|
/// Gets a collection containing the features supported by this context.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract IInvocationFeatures Features { get; }
|
public abstract IInvocationFeatures Features { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="CancellationToken"/> that signals a function invocation is being cancelled.
|
||||||
|
/// </summary>
|
||||||
|
public virtual CancellationToken CancellationToken { get; } = CancellationToken.None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
namespace Microsoft.Azure.Functions.Worker
|
namespace Microsoft.Azure.Functions.Worker
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exposes information about retry acvitity for the event that triggered
|
/// Exposes information about retry activity for the event that triggered
|
||||||
/// the current function invocation.
|
/// the current function invocation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class RetryContext
|
public abstract class RetryContext
|
||||||
|
@ -19,6 +19,5 @@ namespace Microsoft.Azure.Functions.Worker
|
||||||
/// before the event is considered undeliverable.
|
/// before the event is considered undeliverable.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract int MaxRetryCount { get; }
|
public abstract int MaxRetryCount { get; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.Functions.Worker.Converters
|
||||||
|
{
|
||||||
|
internal class CancellationTokenConverter : IInputConverter
|
||||||
|
{
|
||||||
|
public ValueTask<ConversionResult> ConvertAsync(ConverterContext context)
|
||||||
|
{
|
||||||
|
if (context.TargetType == typeof(CancellationToken) || context.TargetType == typeof(CancellationToken?))
|
||||||
|
{
|
||||||
|
return new ValueTask<ConversionResult>(ConversionResult.Success(context.FunctionContext.CancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ValueTask<ConversionResult>(ConversionResult.Unhandled());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Azure.Functions.Worker.Diagnostics;
|
using Microsoft.Azure.Functions.Worker.Diagnostics;
|
||||||
using Microsoft.Azure.Functions.Worker.Middleware;
|
using Microsoft.Azure.Functions.Worker.Middleware;
|
||||||
|
@ -21,8 +22,12 @@ namespace Microsoft.Azure.Functions.Worker
|
||||||
private readonly ILogger<FunctionsApplication> _logger;
|
private readonly ILogger<FunctionsApplication> _logger;
|
||||||
private readonly IWorkerDiagnostics _diagnostics;
|
private readonly IWorkerDiagnostics _diagnostics;
|
||||||
|
|
||||||
public FunctionsApplication(FunctionExecutionDelegate functionExecutionDelegate, IFunctionContextFactory functionContextFactory,
|
public FunctionsApplication(
|
||||||
IOptions<WorkerOptions> workerOptions, ILogger<FunctionsApplication> logger, IWorkerDiagnostics diagnostics)
|
FunctionExecutionDelegate functionExecutionDelegate,
|
||||||
|
IFunctionContextFactory functionContextFactory,
|
||||||
|
IOptions<WorkerOptions> workerOptions,
|
||||||
|
ILogger<FunctionsApplication> logger,
|
||||||
|
IWorkerDiagnostics diagnostics)
|
||||||
{
|
{
|
||||||
_functionExecutionDelegate = functionExecutionDelegate ?? throw new ArgumentNullException(nameof(functionExecutionDelegate));
|
_functionExecutionDelegate = functionExecutionDelegate ?? throw new ArgumentNullException(nameof(functionExecutionDelegate));
|
||||||
_functionContextFactory = functionContextFactory ?? throw new ArgumentNullException(nameof(functionContextFactory));
|
_functionContextFactory = functionContextFactory ?? throw new ArgumentNullException(nameof(functionContextFactory));
|
||||||
|
@ -31,14 +36,14 @@ namespace Microsoft.Azure.Functions.Worker
|
||||||
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
|
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
|
||||||
}
|
}
|
||||||
|
|
||||||
public FunctionContext CreateContext(IInvocationFeatures features)
|
public FunctionContext CreateContext(IInvocationFeatures features, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
var invocation = features.Get<FunctionInvocation>() ?? throw new InvalidOperationException($"The {nameof(FunctionInvocation)} feature is required.");
|
var invocation = features.Get<FunctionInvocation>() ?? throw new InvalidOperationException($"The {nameof(FunctionInvocation)} feature is required.");
|
||||||
|
|
||||||
var functionDefinition = _functionMap[invocation.FunctionId];
|
var functionDefinition = _functionMap[invocation.FunctionId];
|
||||||
features.Set<FunctionDefinition>(functionDefinition);
|
features.Set<FunctionDefinition>(functionDefinition);
|
||||||
|
|
||||||
return _functionContextFactory.Create(features);
|
return _functionContextFactory.Create(features, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadFunction(FunctionDefinition definition)
|
public void LoadFunction(FunctionDefinition definition)
|
||||||
|
|
|
@ -113,13 +113,14 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
workerOption.InputConverters.Register<StringToByteConverter>();
|
workerOption.InputConverters.Register<StringToByteConverter>();
|
||||||
workerOption.InputConverters.Register<JsonPocoConverter>();
|
workerOption.InputConverters.Register<JsonPocoConverter>();
|
||||||
workerOption.InputConverters.Register<ArrayConverter>();
|
workerOption.InputConverters.Register<ArrayConverter>();
|
||||||
|
workerOption.InputConverters.Register<CancellationTokenConverter>();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Run extension startup execution code.
|
/// Run extension startup execution code.
|
||||||
/// Our source generator creates a class(WorkerExtensionStartupCodeExecutor)
|
/// Our source generator creates a class(WorkerExtensionStartupCodeExecutor)
|
||||||
/// which internally calls the "Configure" method on each of the participating
|
/// which internally calls the "Configure" method on each of the participating
|
||||||
/// extensions. Here we are calling the uber "Configure" method on the generated class.
|
/// extensions. Here we are calling the uber "Configure" method on the generated class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void RunExtensionStartupCode(IFunctionsWorkerApplicationBuilder builder)
|
private static void RunExtensionStartupCode(IFunctionsWorkerApplicationBuilder builder)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// 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.
|
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Microsoft.Azure.Functions.Worker
|
namespace Microsoft.Azure.Functions.Worker
|
||||||
|
@ -11,6 +12,6 @@ namespace Microsoft.Azure.Functions.Worker
|
||||||
|
|
||||||
Task InvokeFunctionAsync(FunctionContext context);
|
Task InvokeFunctionAsync(FunctionContext context);
|
||||||
|
|
||||||
FunctionContext CreateContext(IInvocationFeatures features);
|
FunctionContext CreateContext(IInvocationFeatures features, CancellationToken token = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
// 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.
|
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace Microsoft.Azure.Functions.Worker.Pipeline
|
namespace Microsoft.Azure.Functions.Worker.Pipeline
|
||||||
{
|
{
|
||||||
internal interface IFunctionContextFactory
|
internal interface IFunctionContextFactory
|
||||||
{
|
{
|
||||||
FunctionContext Create(IInvocationFeatures features);
|
FunctionContext Create(IInvocationFeatures features, CancellationToken token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.Azure.Functions.Worker.Grpc.Messages;
|
using Microsoft.Azure.Functions.Worker.Grpc.Messages;
|
||||||
|
|
||||||
namespace Microsoft.Azure.Functions.Worker
|
namespace Microsoft.Azure.Functions.Worker
|
||||||
{
|
{
|
||||||
internal class DefaultFunctionMetadataProvider : IFunctionMetadataProvider
|
internal class DefaultFunctionMetadataProvider : IFunctionMetadataProvider
|
||||||
{
|
{
|
||||||
private const string FileName = "functions.metadata";
|
private const string FileName = "functions.metadata";
|
||||||
|
@ -96,7 +96,7 @@ namespace Microsoft.Azure.Functions.Worker
|
||||||
{
|
{
|
||||||
throw new FormatException("Bindings must declare a direction and type.");
|
throw new FormatException("Bindings must declare a direction and type.");
|
||||||
}
|
}
|
||||||
|
|
||||||
BindingInfo bindingInfo = new BindingInfo
|
BindingInfo bindingInfo = new BindingInfo
|
||||||
{
|
{
|
||||||
Direction = direction,
|
Direction = direction,
|
||||||
|
|
|
@ -85,7 +85,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
|
|
||||||
Grpc.Core.Channel grpcChannel = new Grpc.Core.Channel(arguments.Host, arguments.Port, ChannelCredentials.Insecure, options);
|
Grpc.Core.Channel grpcChannel = new Grpc.Core.Channel(arguments.Host, arguments.Port, ChannelCredentials.Insecure, options);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
return new FunctionRpcClient(grpcChannel);
|
return new FunctionRpcClient(grpcChannel);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
|
@ -12,17 +10,18 @@ using Azure.Core.Serialization;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using Microsoft.Azure.Functions.Worker.Context.Features;
|
using Microsoft.Azure.Functions.Worker.Context.Features;
|
||||||
using Microsoft.Azure.Functions.Worker.Grpc;
|
using Microsoft.Azure.Functions.Worker.Grpc;
|
||||||
using Microsoft.Azure.Functions.Worker.Grpc.Features;
|
|
||||||
using Microsoft.Azure.Functions.Worker.Grpc.Messages;
|
using Microsoft.Azure.Functions.Worker.Grpc.Messages;
|
||||||
|
using Microsoft.Azure.Functions.Worker.Handlers;
|
||||||
using Microsoft.Azure.Functions.Worker.Invocation;
|
using Microsoft.Azure.Functions.Worker.Invocation;
|
||||||
|
using Microsoft.Azure.Functions.Worker.Logging;
|
||||||
using Microsoft.Azure.Functions.Worker.OutputBindings;
|
using Microsoft.Azure.Functions.Worker.OutputBindings;
|
||||||
using Microsoft.Azure.Functions.Worker.Rpc;
|
using Microsoft.Azure.Functions.Worker.Rpc;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using static Microsoft.Azure.Functions.Worker.Grpc.Messages.FunctionRpc;
|
using static Microsoft.Azure.Functions.Worker.Grpc.Messages.FunctionRpc;
|
||||||
using MsgType = Microsoft.Azure.Functions.Worker.Grpc.Messages.StreamingMessage.ContentOneofCase;
|
using MsgType = Microsoft.Azure.Functions.Worker.Grpc.Messages.StreamingMessage.ContentOneofCase;
|
||||||
|
|
||||||
|
|
||||||
namespace Microsoft.Azure.Functions.Worker
|
namespace Microsoft.Azure.Functions.Worker
|
||||||
{
|
{
|
||||||
internal class GrpcWorker : IWorker
|
internal class GrpcWorker : IWorker
|
||||||
|
@ -40,13 +39,15 @@ namespace Microsoft.Azure.Functions.Worker
|
||||||
private readonly ObjectSerializer _serializer;
|
private readonly ObjectSerializer _serializer;
|
||||||
private readonly IFunctionMetadataProvider _functionMetadataProvider;
|
private readonly IFunctionMetadataProvider _functionMetadataProvider;
|
||||||
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||||
|
private readonly IInvocationHandler _invocationHandler;
|
||||||
|
|
||||||
public GrpcWorker(IFunctionsApplication application, FunctionRpcClient rpcClient, GrpcHostChannel outputChannel, IInvocationFeaturesFactory invocationFeaturesFactory,
|
public GrpcWorker(IFunctionsApplication application, FunctionRpcClient rpcClient, GrpcHostChannel outputChannel, IInvocationFeaturesFactory invocationFeaturesFactory,
|
||||||
IOutputBindingsInfoProvider outputBindingsInfoProvider, IMethodInfoLocator methodInfoLocator,
|
IOutputBindingsInfoProvider outputBindingsInfoProvider, IMethodInfoLocator methodInfoLocator,
|
||||||
IOptions<GrpcWorkerStartupOptions> startupOptions, IOptions<WorkerOptions> workerOptions,
|
IOptions<GrpcWorkerStartupOptions> startupOptions, IOptions<WorkerOptions> workerOptions,
|
||||||
IInputConversionFeatureProvider inputConversionFeatureProvider,
|
IInputConversionFeatureProvider inputConversionFeatureProvider,
|
||||||
IFunctionMetadataProvider functionMetadataProvider,
|
IFunctionMetadataProvider functionMetadataProvider,
|
||||||
IHostApplicationLifetime hostApplicationLifetime)
|
IHostApplicationLifetime hostApplicationLifetime,
|
||||||
|
ILogger<GrpcWorker> logger)
|
||||||
{
|
{
|
||||||
if (outputChannel == null)
|
if (outputChannel == null)
|
||||||
{
|
{
|
||||||
|
@ -66,6 +67,9 @@ namespace Microsoft.Azure.Functions.Worker
|
||||||
_serializer = workerOptions.Value.Serializer ?? throw new InvalidOperationException(nameof(workerOptions.Value.Serializer));
|
_serializer = workerOptions.Value.Serializer ?? throw new InvalidOperationException(nameof(workerOptions.Value.Serializer));
|
||||||
_inputConversionFeatureProvider = inputConversionFeatureProvider ?? throw new ArgumentNullException(nameof(inputConversionFeatureProvider));
|
_inputConversionFeatureProvider = inputConversionFeatureProvider ?? throw new ArgumentNullException(nameof(inputConversionFeatureProvider));
|
||||||
_functionMetadataProvider = functionMetadataProvider ?? throw new ArgumentNullException(nameof(functionMetadataProvider));
|
_functionMetadataProvider = functionMetadataProvider ?? throw new ArgumentNullException(nameof(functionMetadataProvider));
|
||||||
|
|
||||||
|
// Handlers (TODO: dependency inject handlers instead of creating here)
|
||||||
|
_invocationHandler = new InvocationHandler(_application, _invocationFeaturesFactory, _serializer, _outputBindingsInfoProvider, _inputConversionFeatureProvider, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StartAsync(CancellationToken token)
|
public async Task StartAsync(CancellationToken token)
|
||||||
|
@ -128,125 +132,60 @@ namespace Microsoft.Azure.Functions.Worker
|
||||||
RequestId = request.RequestId
|
RequestId = request.RequestId
|
||||||
};
|
};
|
||||||
|
|
||||||
if (request.ContentCase == MsgType.InvocationRequest)
|
switch(request.ContentCase)
|
||||||
{
|
{
|
||||||
responseMessage.InvocationResponse = await InvocationRequestHandlerAsync(request.InvocationRequest, _application,
|
case MsgType.InvocationRequest:
|
||||||
_invocationFeaturesFactory, _serializer, _outputBindingsInfoProvider, _inputConversionFeatureProvider);
|
responseMessage.InvocationResponse = await InvocationRequestHandlerAsync(request.InvocationRequest);
|
||||||
}
|
break;
|
||||||
else if (request.ContentCase == MsgType.WorkerInitRequest)
|
|
||||||
{
|
case MsgType.WorkerInitRequest:
|
||||||
responseMessage.WorkerInitResponse = WorkerInitRequestHandler(request.WorkerInitRequest);
|
responseMessage.WorkerInitResponse = WorkerInitRequestHandler(request.WorkerInitRequest);
|
||||||
}
|
break;
|
||||||
else if (request.ContentCase == MsgType.WorkerStatusRequest)
|
|
||||||
{
|
case MsgType.WorkerStatusRequest:
|
||||||
responseMessage.WorkerStatusResponse = new WorkerStatusResponse();
|
responseMessage.WorkerStatusResponse = new WorkerStatusResponse();
|
||||||
}
|
break;
|
||||||
else if (request.ContentCase == MsgType.FunctionsMetadataRequest)
|
|
||||||
{
|
case MsgType.FunctionsMetadataRequest:
|
||||||
responseMessage.FunctionMetadataResponse = await GetFunctionMetadataAsync(request.FunctionsMetadataRequest.FunctionAppDirectory);
|
responseMessage.FunctionMetadataResponse = await GetFunctionMetadataAsync(request.FunctionsMetadataRequest.FunctionAppDirectory);
|
||||||
}
|
break;
|
||||||
else if (request.ContentCase == MsgType.WorkerTerminate)
|
|
||||||
{
|
case MsgType.WorkerTerminate:
|
||||||
WorkerTerminateRequestHandler(request.WorkerTerminate);
|
WorkerTerminateRequestHandler(request.WorkerTerminate);
|
||||||
}
|
break;
|
||||||
else if (request.ContentCase == MsgType.FunctionLoadRequest)
|
|
||||||
{
|
case MsgType.FunctionLoadRequest:
|
||||||
responseMessage.FunctionLoadResponse = FunctionLoadRequestHandler(request.FunctionLoadRequest, _application, _methodInfoLocator);
|
responseMessage.FunctionLoadResponse = FunctionLoadRequestHandler(request.FunctionLoadRequest, _application, _methodInfoLocator);
|
||||||
}
|
break;
|
||||||
else if (request.ContentCase == MsgType.FunctionEnvironmentReloadRequest)
|
|
||||||
{
|
case MsgType.FunctionEnvironmentReloadRequest:
|
||||||
// No-op for now, but return a response.
|
// No-op for now, but return a response.
|
||||||
responseMessage.FunctionEnvironmentReloadResponse = new FunctionEnvironmentReloadResponse
|
responseMessage.FunctionEnvironmentReloadResponse = new FunctionEnvironmentReloadResponse
|
||||||
{
|
{
|
||||||
Result = new StatusResult { Status = StatusResult.Types.Status.Success }
|
Result = new StatusResult { Status = StatusResult.Types.Status.Success }
|
||||||
};
|
};
|
||||||
}
|
break;
|
||||||
else
|
|
||||||
{
|
case MsgType.InvocationCancel:
|
||||||
// TODO: Trace failure here.
|
InvocationCancelRequestHandler(request.InvocationCancel);
|
||||||
return;
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// TODO: Trace failure here.
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _outputWriter.WriteAsync(responseMessage);
|
await _outputWriter.WriteAsync(responseMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static async Task<InvocationResponse> InvocationRequestHandlerAsync(InvocationRequest request, IFunctionsApplication application,
|
internal Task<InvocationResponse> InvocationRequestHandlerAsync(InvocationRequest request)
|
||||||
IInvocationFeaturesFactory invocationFeaturesFactory, ObjectSerializer serializer,
|
|
||||||
IOutputBindingsInfoProvider outputBindingsInfoProvider,
|
|
||||||
IInputConversionFeatureProvider functionInputConversionFeatureProvider)
|
|
||||||
{
|
{
|
||||||
FunctionContext? context = null;
|
return _invocationHandler.InvokeAsync(request);
|
||||||
InvocationResponse response = new()
|
}
|
||||||
{
|
|
||||||
InvocationId = request.InvocationId
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
internal void InvocationCancelRequestHandler(InvocationCancel request)
|
||||||
{
|
{
|
||||||
var invocation = new GrpcFunctionInvocation(request);
|
_invocationHandler.TryCancel(request.InvocationId);
|
||||||
|
|
||||||
IInvocationFeatures invocationFeatures = invocationFeaturesFactory.Create();
|
|
||||||
invocationFeatures.Set<FunctionInvocation>(invocation);
|
|
||||||
invocationFeatures.Set<IExecutionRetryFeature>(invocation);
|
|
||||||
|
|
||||||
context = application.CreateContext(invocationFeatures);
|
|
||||||
invocationFeatures.Set<IFunctionBindingsFeature>(new GrpcFunctionBindingsFeature(context, request, outputBindingsInfoProvider));
|
|
||||||
|
|
||||||
if (functionInputConversionFeatureProvider.TryCreate(typeof(DefaultInputConversionFeature), out var conversion))
|
|
||||||
{
|
|
||||||
invocationFeatures.Set<IInputConversionFeature>(conversion!);
|
|
||||||
}
|
|
||||||
|
|
||||||
await application.InvokeFunctionAsync(context);
|
|
||||||
|
|
||||||
var functionBindings = context.GetBindings();
|
|
||||||
|
|
||||||
foreach (var binding in functionBindings.OutputBindingData)
|
|
||||||
{
|
|
||||||
var parameterBinding = new ParameterBinding
|
|
||||||
{
|
|
||||||
Name = binding.Key
|
|
||||||
};
|
|
||||||
|
|
||||||
if (binding.Value is not null)
|
|
||||||
{
|
|
||||||
parameterBinding.Data = await binding.Value.ToRpcAsync(serializer);
|
|
||||||
}
|
|
||||||
|
|
||||||
response.OutputData.Add(parameterBinding);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (functionBindings.InvocationResult != null)
|
|
||||||
{
|
|
||||||
TypedData? returnVal = await functionBindings.InvocationResult.ToRpcAsync(serializer);
|
|
||||||
|
|
||||||
response.ReturnValue = returnVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Result = new StatusResult
|
|
||||||
{
|
|
||||||
Status = StatusResult.Types.Status.Success
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
response.Result = new StatusResult
|
|
||||||
{
|
|
||||||
Exception = ex.ToRpcException(),
|
|
||||||
Status = StatusResult.Types.Status.Failure
|
|
||||||
};
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (context is IAsyncDisposable asyncContext)
|
|
||||||
{
|
|
||||||
await asyncContext.DisposeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
(context as IDisposable)?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static WorkerInitResponse WorkerInitRequestHandler(WorkerInitRequest request)
|
internal static WorkerInitResponse WorkerInitRequestHandler(WorkerInitRequest request)
|
||||||
|
@ -273,6 +212,7 @@ namespace Microsoft.Azure.Functions.Worker
|
||||||
response.Capabilities.Add("TypedDataCollection", bool.TrueString);
|
response.Capabilities.Add("TypedDataCollection", bool.TrueString);
|
||||||
response.Capabilities.Add("WorkerStatus", bool.TrueString);
|
response.Capabilities.Add("WorkerStatus", bool.TrueString);
|
||||||
response.Capabilities.Add("HandlesWorkerTerminateMessage", bool.TrueString);
|
response.Capabilities.Add("HandlesWorkerTerminateMessage", bool.TrueString);
|
||||||
|
response.Capabilities.Add("HandlesInvocationCancelMessage", bool.TrueString);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
// 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.Azure.Functions.Worker.Grpc.Messages;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.Functions.Worker.Handlers
|
||||||
|
{
|
||||||
|
internal interface IInvocationHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes a function based on incoming <see cref="InvocationRequest"/> and returns
|
||||||
|
/// an <see cref="InvocationResponse"/>. This includes creating and keeping track of
|
||||||
|
/// an associated cancellation token source for the invocation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Function invocation request</param>
|
||||||
|
/// <returns><see cref="InvocationResponse"/></returns>
|
||||||
|
Task<InvocationResponse> InvokeAsync(InvocationRequest request);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cancels an invocation's associated <see cref="CancellationTokenSource"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="invocationId">Invocation ID</param>
|
||||||
|
/// <returns>Returns bool indicating success or failure</returns>
|
||||||
|
bool TryCancel(string invocationId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
// 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.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Azure.Core.Serialization;
|
||||||
|
using Microsoft.Azure.Functions.Worker.Context.Features;
|
||||||
|
using Microsoft.Azure.Functions.Worker.Grpc;
|
||||||
|
using Microsoft.Azure.Functions.Worker.Grpc.Features;
|
||||||
|
using Microsoft.Azure.Functions.Worker.Grpc.Messages;
|
||||||
|
using Microsoft.Azure.Functions.Worker.OutputBindings;
|
||||||
|
using Microsoft.Azure.Functions.Worker.Rpc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.Functions.Worker.Handlers
|
||||||
|
{
|
||||||
|
internal class InvocationHandler : IInvocationHandler
|
||||||
|
{
|
||||||
|
private readonly IFunctionsApplication _application;
|
||||||
|
private readonly IInvocationFeaturesFactory _invocationFeaturesFactory;
|
||||||
|
private readonly IOutputBindingsInfoProvider _outputBindingsInfoProvider;
|
||||||
|
private readonly IInputConversionFeatureProvider _inputConversionFeatureProvider;
|
||||||
|
private readonly ObjectSerializer _serializer;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
private ConcurrentDictionary<string, CancellationTokenSource> _inflightInvocations;
|
||||||
|
|
||||||
|
public InvocationHandler(
|
||||||
|
IFunctionsApplication application,
|
||||||
|
IInvocationFeaturesFactory invocationFeaturesFactory,
|
||||||
|
ObjectSerializer serializer,
|
||||||
|
IOutputBindingsInfoProvider outputBindingsInfoProvider,
|
||||||
|
IInputConversionFeatureProvider inputConversionFeatureProvider,
|
||||||
|
ILogger logger)
|
||||||
|
{
|
||||||
|
_application = application ?? throw new ArgumentNullException(nameof(application));
|
||||||
|
_invocationFeaturesFactory = invocationFeaturesFactory ?? throw new ArgumentNullException(nameof(invocationFeaturesFactory));
|
||||||
|
_serializer = serializer ?? throw new ArgumentNullException(nameof(serializer));
|
||||||
|
_outputBindingsInfoProvider = outputBindingsInfoProvider ?? throw new ArgumentNullException(nameof(outputBindingsInfoProvider));
|
||||||
|
_inputConversionFeatureProvider = inputConversionFeatureProvider ?? throw new ArgumentNullException(nameof(inputConversionFeatureProvider));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
|
||||||
|
_inflightInvocations = new ConcurrentDictionary<string, CancellationTokenSource>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<InvocationResponse> InvokeAsync(InvocationRequest request)
|
||||||
|
{
|
||||||
|
using CancellationTokenSource cancellationTokenSource = new();
|
||||||
|
FunctionContext? context = null;
|
||||||
|
InvocationResponse response = new()
|
||||||
|
{
|
||||||
|
InvocationId = request.InvocationId,
|
||||||
|
Result = new StatusResult()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!_inflightInvocations.TryAdd(request.InvocationId, cancellationTokenSource))
|
||||||
|
{
|
||||||
|
var exception = new InvalidOperationException("Unable to track CancellationTokenSource");
|
||||||
|
response.Result.Status = StatusResult.Types.Status.Failure;
|
||||||
|
response.Result.Exception = exception.ToRpcException();
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var invocation = new GrpcFunctionInvocation(request);
|
||||||
|
|
||||||
|
IInvocationFeatures invocationFeatures = _invocationFeaturesFactory.Create();
|
||||||
|
invocationFeatures.Set<FunctionInvocation>(invocation);
|
||||||
|
invocationFeatures.Set<IExecutionRetryFeature>(invocation);
|
||||||
|
|
||||||
|
context = _application.CreateContext(invocationFeatures, cancellationTokenSource.Token);
|
||||||
|
invocationFeatures.Set<IFunctionBindingsFeature>(new GrpcFunctionBindingsFeature(context, request, _outputBindingsInfoProvider));
|
||||||
|
|
||||||
|
if (_inputConversionFeatureProvider.TryCreate(typeof(DefaultInputConversionFeature), out var conversion))
|
||||||
|
{
|
||||||
|
invocationFeatures.Set<IInputConversionFeature>(conversion!);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _application.InvokeFunctionAsync(context);
|
||||||
|
|
||||||
|
var functionBindings = context.GetBindings();
|
||||||
|
|
||||||
|
foreach (var binding in functionBindings.OutputBindingData)
|
||||||
|
{
|
||||||
|
var parameterBinding = new ParameterBinding
|
||||||
|
{
|
||||||
|
Name = binding.Key
|
||||||
|
};
|
||||||
|
|
||||||
|
if (binding.Value is not null)
|
||||||
|
{
|
||||||
|
parameterBinding.Data = await binding.Value.ToRpcAsync(_serializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OutputData.Add(parameterBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (functionBindings.InvocationResult is not null)
|
||||||
|
{
|
||||||
|
TypedData? returnVal = await functionBindings.InvocationResult.ToRpcAsync(_serializer);
|
||||||
|
response.ReturnValue = returnVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Result.Status = StatusResult.Types.Status.Success;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
response.Result.Exception = ex.ToRpcException();
|
||||||
|
response.Result.Status = StatusResult.Types.Status.Failure;
|
||||||
|
|
||||||
|
if (ex.InnerException is TaskCanceledException or OperationCanceledException)
|
||||||
|
{
|
||||||
|
response.Result.Status = StatusResult.Types.Status.Cancelled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_inflightInvocations.TryRemove(request.InvocationId, out var cts);
|
||||||
|
|
||||||
|
if (context is IAsyncDisposable asyncContext)
|
||||||
|
{
|
||||||
|
await asyncContext.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
(context as IDisposable)?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryCancel(string invocationId)
|
||||||
|
{
|
||||||
|
if (_inflightInvocations.TryGetValue(invocationId, out var cancellationTokenSource))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cancellationTokenSource?.Cancel();
|
||||||
|
_logger.LogWarning("Unable to cancel invocation {invocationId}.", invocationId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// Do nothing, normal behavior
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Unable to cancel invocation {invocationId}.", invocationId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
// 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.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Azure.Functions.Worker.Converters;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.Functions.Worker.Tests.Converters
|
||||||
|
{
|
||||||
|
public class CancellationTokenConverterTests
|
||||||
|
{
|
||||||
|
private CancellationTokenConverter _converter = new CancellationTokenConverter();
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(CancellationToken))]
|
||||||
|
[InlineData(typeof(CancellationToken?))]
|
||||||
|
public async Task ConversionSuccessfulForValidTargetTypeAsync(Type targetType)
|
||||||
|
{
|
||||||
|
var context = new TestConverterContext(targetType, null);
|
||||||
|
var conversionResult = await _converter.ConvertAsync(context);
|
||||||
|
|
||||||
|
Assert.Equal(ConversionStatus.Succeeded, conversionResult.Status);
|
||||||
|
Assert.Equal(typeof(CancellationToken), conversionResult.Value.GetType());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(Guid))]
|
||||||
|
[InlineData(typeof(String))]
|
||||||
|
[InlineData(typeof(Object))]
|
||||||
|
[InlineData(typeof(DateTimeOffset))]
|
||||||
|
public async Task ConversionFailedForInvalidTargetType(Type targetType)
|
||||||
|
{
|
||||||
|
var context = new TestConverterContext(targetType, null);
|
||||||
|
var conversionResult = await _converter.ConvertAsync(context);
|
||||||
|
|
||||||
|
Assert.Equal(ConversionStatus.Unhandled, conversionResult.Status);
|
||||||
|
Assert.Null(conversionResult.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
@ -33,7 +34,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
features.Set<FunctionDefinition>(definition);
|
features.Set<FunctionDefinition>(definition);
|
||||||
features.Set<FunctionInvocation>(invocation);
|
features.Set<FunctionInvocation>(invocation);
|
||||||
|
|
||||||
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, features);
|
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, features, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
@ -7,6 +7,7 @@ using System.Collections.Immutable;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Azure.Functions.Worker.Context.Features;
|
using Microsoft.Azure.Functions.Worker.Context.Features;
|
||||||
using Microsoft.Azure.Functions.Worker.Converters;
|
using Microsoft.Azure.Functions.Worker.Converters;
|
||||||
|
@ -197,7 +198,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
mockServiceProvider.Setup(a => a.GetService(typeof(IBindingCache<ConversionResult>)))
|
mockServiceProvider.Setup(a => a.GetService(typeof(IBindingCache<ConversionResult>)))
|
||||||
.Returns(new DefaultBindingCache<ConversionResult>());
|
.Returns(new DefaultBindingCache<ConversionResult>());
|
||||||
|
|
||||||
return new TestFunctionContext(definition, invocation, serviceProvider: mockServiceProvider.Object);
|
return new TestFunctionContext(definition, invocation, CancellationToken.None, serviceProvider: mockServiceProvider.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Functions
|
private class Functions
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
using Azure.Core.Serialization;
|
using Azure.Core.Serialization;
|
||||||
using Microsoft.Azure.Functions.Worker.Context.Features;
|
using Microsoft.Azure.Functions.Worker.Context.Features;
|
||||||
using Microsoft.Azure.Functions.Worker.Tests.Features;
|
using Microsoft.Azure.Functions.Worker.Tests.Features;
|
||||||
|
@ -46,7 +47,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
{ "myQueueItem", new TestBindingMetadata("myQueueItem","queueTrigger",BindingDirection.In) },
|
{ "myQueueItem", new TestBindingMetadata("myQueueItem","queueTrigger",BindingDirection.In) },
|
||||||
{ "myGuid", new TestBindingMetadata("myGuid","queueTrigger",BindingDirection.In) }
|
{ "myGuid", new TestBindingMetadata("myGuid","queueTrigger",BindingDirection.In) }
|
||||||
});
|
});
|
||||||
var functionContext = new TestFunctionContext(definition, invocation: null, serviceProvider: _serviceProvider, features: features);
|
var functionContext = new TestFunctionContext(definition, invocation: null, CancellationToken.None, serviceProvider: _serviceProvider, features: features);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var parameterValuesArray = await _modelBindingFeature.BindFunctionInputAsync(functionContext);
|
var parameterValuesArray = await _modelBindingFeature.BindFunctionInputAsync(functionContext);
|
||||||
|
@ -85,7 +86,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
{
|
{
|
||||||
{ "myQueueItem", new TestBindingMetadata("myQueueItem","queueTrigger",BindingDirection.In) }
|
{ "myQueueItem", new TestBindingMetadata("myQueueItem","queueTrigger",BindingDirection.In) }
|
||||||
});
|
});
|
||||||
var functionContext = new TestFunctionContext(definition, invocation: null, serviceProvider: _serviceProvider, features: features);
|
var functionContext = new TestFunctionContext(definition, invocation: null, CancellationToken.None, serviceProvider: _serviceProvider, features: features);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
// bind to the queue trigger input binding item.
|
// bind to the queue trigger input binding item.
|
||||||
|
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Azure.Functions.Worker.Context.Features;
|
using Microsoft.Azure.Functions.Worker.Context.Features;
|
||||||
using Microsoft.Azure.Functions.Worker.Converters;
|
using Microsoft.Azure.Functions.Worker.Converters;
|
||||||
|
@ -57,7 +58,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
{ "blob2", new TestBindingMetadata("blob2","blob",BindingDirection.In) }
|
{ "blob2", new TestBindingMetadata("blob2","blob",BindingDirection.In) }
|
||||||
});
|
});
|
||||||
_features.Set<FunctionDefinition>(qTriggerFunctionDefinition);
|
_features.Set<FunctionDefinition>(qTriggerFunctionDefinition);
|
||||||
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features);
|
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features, CancellationToken.None);
|
||||||
|
|
||||||
var functionBindings = new TestFunctionBindingsFeature
|
var functionBindings = new TestFunctionBindingsFeature
|
||||||
{
|
{
|
||||||
|
@ -111,7 +112,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
{ "myQueueItem", new TestBindingMetadata("myQueueItem","queueTrigger",BindingDirection.In) }
|
{ "myQueueItem", new TestBindingMetadata("myQueueItem","queueTrigger",BindingDirection.In) }
|
||||||
});
|
});
|
||||||
_features.Set<FunctionDefinition>(qTriggerFunctionDefinition);
|
_features.Set<FunctionDefinition>(qTriggerFunctionDefinition);
|
||||||
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features);
|
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features, CancellationToken.None);
|
||||||
|
|
||||||
var functionBindings = new TestFunctionBindingsFeature
|
var functionBindings = new TestFunctionBindingsFeature
|
||||||
{
|
{
|
||||||
|
@ -154,7 +155,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
{ "myQueueItem", new TestBindingMetadata("myQueueItem","queueTrigger",BindingDirection.In) }
|
{ "myQueueItem", new TestBindingMetadata("myQueueItem","queueTrigger",BindingDirection.In) }
|
||||||
});
|
});
|
||||||
_features.Set<FunctionDefinition>(qTriggerFunctionDefinition);
|
_features.Set<FunctionDefinition>(qTriggerFunctionDefinition);
|
||||||
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features);
|
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features, CancellationToken.None);
|
||||||
|
|
||||||
var functionBindings = new TestFunctionBindingsFeature
|
var functionBindings = new TestFunctionBindingsFeature
|
||||||
{
|
{
|
||||||
|
@ -182,7 +183,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
});
|
});
|
||||||
_features.Set<FunctionDefinition>(functionDefinition);
|
_features.Set<FunctionDefinition>(functionDefinition);
|
||||||
|
|
||||||
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features);
|
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features, CancellationToken.None);
|
||||||
|
|
||||||
var functionBindings = new TestFunctionBindingsFeature();
|
var functionBindings = new TestFunctionBindingsFeature();
|
||||||
var grpcHttpResponse = new GrpcHttpResponseData(_defaultFunctionContext, HttpStatusCode.OK);
|
var grpcHttpResponse = new GrpcHttpResponseData(_defaultFunctionContext, HttpStatusCode.OK);
|
||||||
|
@ -227,7 +228,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
});
|
});
|
||||||
_features.Set<FunctionDefinition>(functionDefinition);
|
_features.Set<FunctionDefinition>(functionDefinition);
|
||||||
|
|
||||||
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features);
|
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features, CancellationToken.None);
|
||||||
|
|
||||||
var functionBindings = new TestFunctionBindingsFeature();
|
var functionBindings = new TestFunctionBindingsFeature();
|
||||||
// Not setting any values to functionBindings.OutputBindingData dictionary.
|
// Not setting any values to functionBindings.OutputBindingData dictionary.
|
||||||
|
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Azure.Functions.Worker.Context.Features;
|
using Microsoft.Azure.Functions.Worker.Context.Features;
|
||||||
using Microsoft.Azure.Functions.Worker.Converters;
|
using Microsoft.Azure.Functions.Worker.Converters;
|
||||||
|
@ -55,7 +56,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
});
|
});
|
||||||
_features.Set<FunctionDefinition>(qTriggerFunctionDefinition);
|
_features.Set<FunctionDefinition>(qTriggerFunctionDefinition);
|
||||||
|
|
||||||
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features);
|
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features, CancellationToken.None);
|
||||||
var functionBindings = new TestFunctionBindingsFeature
|
var functionBindings = new TestFunctionBindingsFeature
|
||||||
{
|
{
|
||||||
InputData = new ReadOnlyDictionary<string, object>(new Dictionary<string, object> { { "myQueueItem", "foo" } })
|
InputData = new ReadOnlyDictionary<string, object>(new Dictionary<string, object> { { "myQueueItem", "foo" } })
|
||||||
|
@ -83,7 +84,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
});
|
});
|
||||||
_features.Set<FunctionDefinition>(httpFunctionDefinition);
|
_features.Set<FunctionDefinition>(httpFunctionDefinition);
|
||||||
|
|
||||||
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features);
|
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features, CancellationToken.None);
|
||||||
var grpcHttpReq = new GrpcHttpRequestData(CreateRpcHttp(), _defaultFunctionContext);
|
var grpcHttpReq = new GrpcHttpRequestData(CreateRpcHttp(), _defaultFunctionContext);
|
||||||
var functionBindings = new TestFunctionBindingsFeature
|
var functionBindings = new TestFunctionBindingsFeature
|
||||||
{
|
{
|
||||||
|
@ -91,7 +92,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
};
|
};
|
||||||
_features.Set<IFunctionBindingsFeature>(functionBindings);
|
_features.Set<IFunctionBindingsFeature>(functionBindings);
|
||||||
|
|
||||||
// Mock input conversion feature to return a successfully converted HttpRequestData value.
|
// Mock input conversion feature to return a successfully converted HttpRequestData value.
|
||||||
_mockConversionFeature.Setup(a => a.ConvertAsync(It.IsAny<ConverterContext>()))
|
_mockConversionFeature.Setup(a => a.ConvertAsync(It.IsAny<ConverterContext>()))
|
||||||
.ReturnsAsync(ConversionResult.Success(grpcHttpReq));
|
.ReturnsAsync(ConversionResult.Success(grpcHttpReq));
|
||||||
_features.Set<IInputConversionFeature>(_mockConversionFeature.Object);
|
_features.Set<IInputConversionFeature>(_mockConversionFeature.Object);
|
||||||
|
@ -116,7 +117,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
// Arrange
|
// Arrange
|
||||||
_features.Set<FunctionDefinition>(new TestFunctionDefinition());
|
_features.Set<FunctionDefinition>(new TestFunctionDefinition());
|
||||||
|
|
||||||
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features);
|
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features, CancellationToken.None);
|
||||||
var grpcHttpReq = new GrpcHttpRequestData(CreateRpcHttp(), _defaultFunctionContext);
|
var grpcHttpReq = new GrpcHttpRequestData(CreateRpcHttp(), _defaultFunctionContext);
|
||||||
var functionBindings = new TestFunctionBindingsFeature
|
var functionBindings = new TestFunctionBindingsFeature
|
||||||
{
|
{
|
||||||
|
@ -151,7 +152,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
{ "MyHttpResponse", new TestBindingMetadata("MyHttpResponse","http",BindingDirection.Out) }
|
{ "MyHttpResponse", new TestBindingMetadata("MyHttpResponse","http",BindingDirection.Out) }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features);
|
_defaultFunctionContext = new DefaultFunctionContext(_serviceScopeFactory, _features, CancellationToken.None);
|
||||||
var grpcHttpReq = new GrpcHttpRequestData(CreateRpcHttp(), _defaultFunctionContext);
|
var grpcHttpReq = new GrpcHttpRequestData(CreateRpcHttp(), _defaultFunctionContext);
|
||||||
var functionBindings = new TestFunctionBindingsFeature
|
var functionBindings = new TestFunctionBindingsFeature
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
using Microsoft.Azure.Functions.Worker.Grpc.Messages;
|
using Microsoft.Azure.Functions.Worker.Grpc.Messages;
|
||||||
using Microsoft.Azure.Functions.Worker.Http;
|
using Microsoft.Azure.Functions.Worker.Http;
|
||||||
using Microsoft.Azure.Functions.Worker.Invocation;
|
using Microsoft.Azure.Functions.Worker.Invocation;
|
||||||
|
@ -77,5 +78,13 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
return req.CreateResponse();
|
return req.CreateResponse();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class MyFunctionClassWithCancellation
|
||||||
|
{
|
||||||
|
public HttpResponseData Run(HttpRequestData req, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return req.CreateResponse();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,15 @@ using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Azure.Core.Serialization;
|
using Azure.Core.Serialization;
|
||||||
using Microsoft.Azure.Functions.Worker.Context.Features;
|
using Microsoft.Azure.Functions.Worker.Context.Features;
|
||||||
using Microsoft.Azure.Functions.Worker.Grpc.Messages;
|
using Microsoft.Azure.Functions.Worker.Grpc.Messages;
|
||||||
|
using Microsoft.Azure.Functions.Worker.Handlers;
|
||||||
using Microsoft.Azure.Functions.Worker.Invocation;
|
using Microsoft.Azure.Functions.Worker.Invocation;
|
||||||
using Microsoft.Azure.Functions.Worker.OutputBindings;
|
using Microsoft.Azure.Functions.Worker.OutputBindings;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
@ -26,6 +29,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
private readonly Mock<IMethodInfoLocator> _mockMethodInfoLocator = new(MockBehavior.Strict);
|
private readonly Mock<IMethodInfoLocator> _mockMethodInfoLocator = new(MockBehavior.Strict);
|
||||||
private TestFunctionContext _context = new();
|
private TestFunctionContext _context = new();
|
||||||
private TestAsyncFunctionContext _asyncContext = new();
|
private TestAsyncFunctionContext _asyncContext = new();
|
||||||
|
private ILogger<InvocationHandler> _testLogger;
|
||||||
|
|
||||||
public GrpcWorkerTests()
|
public GrpcWorkerTests()
|
||||||
{
|
{
|
||||||
|
@ -33,10 +37,9 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
.Setup(m => m.LoadFunction(It.IsAny<FunctionDefinition>()));
|
.Setup(m => m.LoadFunction(It.IsAny<FunctionDefinition>()));
|
||||||
|
|
||||||
_mockApplication
|
_mockApplication
|
||||||
.Setup(m => m.CreateContext(It.IsAny<IInvocationFeatures>()))
|
.Setup(m => m.CreateContext(It.IsAny<IInvocationFeatures>(), It.IsAny<CancellationToken>()))
|
||||||
.Returns<IInvocationFeatures>(f =>
|
.Returns((IInvocationFeatures f, CancellationToken ct) => {
|
||||||
{
|
_context = new TestFunctionContext(f, ct);
|
||||||
_context = new TestFunctionContext(f);
|
|
||||||
return _context;
|
return _context;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -54,8 +57,10 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
|
|
||||||
IInputConversionFeature conversionFeature = mockConversionFeature.Object;
|
IInputConversionFeature conversionFeature = mockConversionFeature.Object;
|
||||||
_mockInputConversionFeatureProvider
|
_mockInputConversionFeatureProvider
|
||||||
.Setup(m=>m.TryCreate(typeof(DefaultInputConversionFeature), out conversionFeature))
|
.Setup(m => m.TryCreate(typeof(DefaultInputConversionFeature), out conversionFeature))
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
|
||||||
|
_testLogger = TestLoggerProvider.Factory.CreateLogger<InvocationHandler>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
@ -130,10 +135,13 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Invoke_ReturnsSuccess()
|
public async Task Invoke_ReturnsSuccess()
|
||||||
{
|
{
|
||||||
var request = CreateInvocationRequest();
|
var request = TestUtility.CreateInvocationRequest();
|
||||||
|
|
||||||
var response = await GrpcWorker.InvocationRequestHandlerAsync(request, _mockApplication.Object, _mockFeaturesFactory.Object,
|
var invocationHandler = new InvocationHandler(_mockApplication.Object,
|
||||||
new JsonObjectSerializer(), _mockOutputBindingsInfoProvider.Object, _mockInputConversionFeatureProvider.Object);
|
_mockFeaturesFactory.Object, new JsonObjectSerializer(), _mockOutputBindingsInfoProvider.Object,
|
||||||
|
_mockInputConversionFeatureProvider.Object, _testLogger);
|
||||||
|
|
||||||
|
var response = await invocationHandler.InvokeAsync(request);
|
||||||
|
|
||||||
Assert.Equal(StatusResult.Types.Status.Success, response.Result.Status);
|
Assert.Equal(StatusResult.Types.Status.Success, response.Result.Status);
|
||||||
Assert.True(_context.IsDisposed);
|
Assert.True(_context.IsDisposed);
|
||||||
|
@ -142,19 +150,22 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Invoke_ReturnsSuccess_AsyncFunctionContext()
|
public async Task Invoke_ReturnsSuccess_AsyncFunctionContext()
|
||||||
{
|
{
|
||||||
var request = CreateInvocationRequest();
|
var request = TestUtility.CreateInvocationRequest();
|
||||||
|
|
||||||
// Mock IFunctionApplication.CreateContext to return TestAsyncFunctionContext instance.
|
// Mock IFunctionApplication.CreateContext to return TestAsyncFunctionContext instance.
|
||||||
_mockApplication
|
_mockApplication
|
||||||
.Setup(m => m.CreateContext(It.IsAny<IInvocationFeatures>()))
|
.Setup(m => m.CreateContext(It.IsAny<IInvocationFeatures>(), It.IsAny<CancellationToken>()))
|
||||||
.Returns<IInvocationFeatures>(f =>
|
.Returns<IInvocationFeatures, CancellationToken>((f,ct) =>
|
||||||
{
|
{
|
||||||
_context = new TestAsyncFunctionContext(f);
|
_context = new TestAsyncFunctionContext(f);
|
||||||
return _context;
|
return _context;
|
||||||
});
|
});
|
||||||
|
|
||||||
var response = await GrpcWorker.InvocationRequestHandlerAsync(request, _mockApplication.Object, _mockFeaturesFactory.Object,
|
var invocationHandler = new InvocationHandler(_mockApplication.Object,
|
||||||
new JsonObjectSerializer(), _mockOutputBindingsInfoProvider.Object, _mockInputConversionFeatureProvider.Object);
|
_mockFeaturesFactory.Object, new JsonObjectSerializer(), _mockOutputBindingsInfoProvider.Object,
|
||||||
|
_mockInputConversionFeatureProvider.Object, _testLogger);
|
||||||
|
|
||||||
|
var response = await invocationHandler.InvokeAsync(request);
|
||||||
|
|
||||||
Assert.Equal(StatusResult.Types.Status.Success, response.Result.Status);
|
Assert.Equal(StatusResult.Types.Status.Success, response.Result.Status);
|
||||||
Assert.True((_context as TestAsyncFunctionContext).IsAsyncDisposed);
|
Assert.True((_context as TestAsyncFunctionContext).IsAsyncDisposed);
|
||||||
|
@ -164,11 +175,14 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Invoke_SetsRetryContext()
|
public async Task Invoke_SetsRetryContext()
|
||||||
{
|
{
|
||||||
var request = CreateInvocationRequest();
|
var request = TestUtility.CreateInvocationRequest();
|
||||||
|
|
||||||
|
var invocationHandler = new InvocationHandler(_mockApplication.Object,
|
||||||
|
_mockFeaturesFactory.Object, new JsonObjectSerializer(), _mockOutputBindingsInfoProvider.Object,
|
||||||
|
_mockInputConversionFeatureProvider.Object, _testLogger);
|
||||||
|
|
||||||
|
var response = await invocationHandler.InvokeAsync(request);
|
||||||
|
|
||||||
var response = await GrpcWorker.InvocationRequestHandlerAsync(request, _mockApplication.Object, _mockFeaturesFactory.Object,
|
|
||||||
new JsonObjectSerializer(), _mockOutputBindingsInfoProvider.Object, _mockInputConversionFeatureProvider.Object);
|
|
||||||
|
|
||||||
Assert.Equal(StatusResult.Types.Status.Success, response.Result.Status);
|
Assert.Equal(StatusResult.Types.Status.Success, response.Result.Status);
|
||||||
Assert.True(_context.IsDisposed);
|
Assert.True(_context.IsDisposed);
|
||||||
Assert.Equal(request.RetryContext.RetryCount, _context.RetryContext.RetryCount);
|
Assert.Equal(request.RetryContext.RetryCount, _context.RetryContext.RetryCount);
|
||||||
|
@ -179,36 +193,22 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
public async Task Invoke_CreateContextThrows_ReturnsFailure()
|
public async Task Invoke_CreateContextThrows_ReturnsFailure()
|
||||||
{
|
{
|
||||||
_mockApplication
|
_mockApplication
|
||||||
.Setup(m => m.CreateContext(It.IsAny<IInvocationFeatures>()))
|
.Setup(m => m.CreateContext(It.IsAny<IInvocationFeatures>(), It.IsAny<CancellationToken>()))
|
||||||
.Throws(new InvalidOperationException("whoops"));
|
.Throws(new InvalidOperationException("whoops"));
|
||||||
|
|
||||||
var request = CreateInvocationRequest();
|
var request = TestUtility.CreateInvocationRequest();
|
||||||
|
|
||||||
var response = await GrpcWorker.InvocationRequestHandlerAsync(request, _mockApplication.Object, _mockFeaturesFactory.Object,
|
var invocationHandler = new InvocationHandler(_mockApplication.Object,
|
||||||
new JsonObjectSerializer(), _mockOutputBindingsInfoProvider.Object, _mockInputConversionFeatureProvider.Object);
|
_mockFeaturesFactory.Object, new JsonObjectSerializer(), _mockOutputBindingsInfoProvider.Object,
|
||||||
|
_mockInputConversionFeatureProvider.Object, _testLogger);
|
||||||
|
|
||||||
|
var response = await invocationHandler.InvokeAsync(request);
|
||||||
|
|
||||||
Assert.Equal(StatusResult.Types.Status.Failure, response.Result.Status);
|
Assert.Equal(StatusResult.Types.Status.Failure, response.Result.Status);
|
||||||
Assert.Contains("InvalidOperationException: whoops", response.Result.Exception.Message);
|
Assert.Contains("InvalidOperationException: whoops", response.Result.Exception.Message);
|
||||||
Assert.Contains("CreateContext", response.Result.Exception.Message);
|
Assert.Contains("CreateContext", response.Result.Exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Invoke_InvokeAsyncThrows_ReturnsFailure()
|
|
||||||
{
|
|
||||||
_mockApplication
|
|
||||||
.Setup(m => m.InvokeFunctionAsync(It.IsAny<FunctionContext>()))
|
|
||||||
.Throws(new InvalidOperationException("whoops"));
|
|
||||||
|
|
||||||
var request = CreateInvocationRequest();
|
|
||||||
|
|
||||||
var response = await GrpcWorker.InvocationRequestHandlerAsync(request, _mockApplication.Object, _mockFeaturesFactory.Object,
|
|
||||||
new JsonObjectSerializer(), _mockOutputBindingsInfoProvider.Object, _mockInputConversionFeatureProvider.Object);
|
|
||||||
|
|
||||||
Assert.Equal(StatusResult.Types.Status.Failure, response.Result.Status);
|
|
||||||
Assert.Contains("InvalidOperationException: whoops", response.Result.Exception.Message);
|
|
||||||
Assert.Contains("InvokeFunctionAsync", response.Result.Exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static FunctionLoadRequest CreateFunctionLoadRequest()
|
private static FunctionLoadRequest CreateFunctionLoadRequest()
|
||||||
{
|
{
|
||||||
return new FunctionLoadRequest
|
return new FunctionLoadRequest
|
||||||
|
@ -220,23 +220,6 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static InvocationRequest CreateInvocationRequest()
|
|
||||||
{
|
|
||||||
return new InvocationRequest
|
|
||||||
{
|
|
||||||
TraceContext = new RpcTraceContext
|
|
||||||
{
|
|
||||||
TraceParent = Guid.NewGuid().ToString(),
|
|
||||||
TraceState = Guid.NewGuid().ToString()
|
|
||||||
},
|
|
||||||
RetryContext = new Grpc.Messages.RetryContext
|
|
||||||
{
|
|
||||||
MaxRetryCount = 3,
|
|
||||||
RetryCount = 2
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used for MethodInfo in tests
|
// Used for MethodInfo in tests
|
||||||
private void TestRun()
|
private void TestRun()
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
// 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.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Azure.Core.Serialization;
|
||||||
|
using Microsoft.Azure.Functions.Worker.Context.Features;
|
||||||
|
using Microsoft.Azure.Functions.Worker.Grpc.Messages;
|
||||||
|
using Microsoft.Azure.Functions.Worker.Handlers;
|
||||||
|
using Microsoft.Azure.Functions.Worker.OutputBindings;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
|
{
|
||||||
|
public class InvocationHandlerTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IFunctionsApplication> _mockApplication = new(MockBehavior.Strict);
|
||||||
|
private readonly Mock<IInvocationFeaturesFactory> _mockInvocationFeaturesFactory = new(MockBehavior.Strict);
|
||||||
|
private readonly Mock<IOutputBindingsInfoProvider> _mockOutputBindingsInfoProvider = new(MockBehavior.Strict);
|
||||||
|
private readonly Mock<IInputConversionFeatureProvider> _mockInputConversionFeatureProvider = new(MockBehavior.Strict);
|
||||||
|
private readonly Mock<IInputConversionFeature> mockConversionFeature = new(MockBehavior.Strict);
|
||||||
|
private TestFunctionContext _context = new();
|
||||||
|
private ILogger<InvocationHandler> _testLogger;
|
||||||
|
|
||||||
|
public InvocationHandlerTests()
|
||||||
|
{
|
||||||
|
_mockApplication
|
||||||
|
.Setup(m => m.CreateContext(It.IsAny<IInvocationFeatures>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns((IInvocationFeatures f, CancellationToken ct) => {
|
||||||
|
_context = new TestFunctionContext(f, ct);
|
||||||
|
return _context;
|
||||||
|
});
|
||||||
|
|
||||||
|
_mockApplication
|
||||||
|
.Setup(m => m.InvokeFunctionAsync(It.IsAny<FunctionContext>()))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
_mockInvocationFeaturesFactory
|
||||||
|
.Setup(m => m.Create())
|
||||||
|
.Returns(new InvocationFeatures(Enumerable.Empty<IInvocationFeatureProvider>()));
|
||||||
|
|
||||||
|
IInputConversionFeature conversionFeature = mockConversionFeature.Object;
|
||||||
|
_mockInputConversionFeatureProvider
|
||||||
|
.Setup(m => m.TryCreate(typeof(DefaultInputConversionFeature), out conversionFeature))
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
_testLogger = TestLoggerProvider.Factory.CreateLogger<InvocationHandler>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_CreatesValidCancellationToken_ReturnsSuccess()
|
||||||
|
{
|
||||||
|
var invocationId = "5fb3a9b4-0b38-450a-9d46-35946e7edea7";
|
||||||
|
var request = TestUtility.CreateInvocationRequest(invocationId);
|
||||||
|
|
||||||
|
var handler = new InvocationHandler(_mockApplication.Object,
|
||||||
|
_mockInvocationFeaturesFactory.Object, new JsonObjectSerializer(), _mockOutputBindingsInfoProvider.Object,
|
||||||
|
_mockInputConversionFeatureProvider.Object, _testLogger);
|
||||||
|
|
||||||
|
var response = await handler.InvokeAsync(request);
|
||||||
|
|
||||||
|
// InvokeAsync should create a real cancellation token which can be cancelled,
|
||||||
|
// otherwise we set the token to be CancellationToken.Empty which **cannot** be cancelled
|
||||||
|
Assert.Equal(StatusResult.Types.Status.Success, response.Result.Status);
|
||||||
|
Assert.True(_context.CancellationToken.CanBeCanceled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_ThrowsTaskCanceledException_ReturnsCancelled()
|
||||||
|
{
|
||||||
|
_mockApplication
|
||||||
|
.Setup(m => m.InvokeFunctionAsync(It.IsAny<FunctionContext>()))
|
||||||
|
.Throws(new AggregateException(new Exception[] { new TaskCanceledException() }));
|
||||||
|
|
||||||
|
var request = TestUtility.CreateInvocationRequest("abc");
|
||||||
|
|
||||||
|
var invocationHandler = new InvocationHandler(_mockApplication.Object,
|
||||||
|
_mockInvocationFeaturesFactory.Object, new JsonObjectSerializer(), _mockOutputBindingsInfoProvider.Object,
|
||||||
|
_mockInputConversionFeatureProvider.Object, _testLogger);
|
||||||
|
|
||||||
|
var response = await invocationHandler.InvokeAsync(request);
|
||||||
|
|
||||||
|
Assert.Equal(StatusResult.Types.Status.Cancelled, response.Result.Status);
|
||||||
|
Assert.Contains("TaskCanceledException", response.Result.Exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Cancel_InvocationInProgress_CancelsTokenSource_ReturnsTrue()
|
||||||
|
{
|
||||||
|
var invocationId = "5fb3a9b4-0b38-450a-9d46-35946e7edea7";
|
||||||
|
var request = TestUtility.CreateInvocationRequest(invocationId);
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
var handler = new InvocationHandler(_mockApplication.Object,
|
||||||
|
_mockInvocationFeaturesFactory.Object, new JsonObjectSerializer(), _mockOutputBindingsInfoProvider.Object,
|
||||||
|
_mockInputConversionFeatureProvider.Object, _testLogger);
|
||||||
|
|
||||||
|
// Mock delay in InvokeFunctionAsync so that we can cancel mid invocation
|
||||||
|
_mockApplication
|
||||||
|
.Setup(m => m.InvokeFunctionAsync(It.IsAny<FunctionContext>()))
|
||||||
|
.Callback(() => Thread.Sleep(1000))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
// Don't wait for InvokeAsync so we can cancel whilst it's in progress
|
||||||
|
_ = Task.Run(async () => {
|
||||||
|
await handler.InvokeAsync(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Buffer to ensure the cancellation token source was created before we try to cancel
|
||||||
|
Thread.Sleep(500);
|
||||||
|
|
||||||
|
var result = handler.TryCancel(invocationId);
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Cancel_InvocationCompleted_ReturnsFalse()
|
||||||
|
{
|
||||||
|
var invocationId = "5fb3a9b4-0b38-450a-9d46-35946e7edea7";
|
||||||
|
var request = TestUtility.CreateInvocationRequest(invocationId);
|
||||||
|
|
||||||
|
var handler = new InvocationHandler(_mockApplication.Object,
|
||||||
|
_mockInvocationFeaturesFactory.Object, new JsonObjectSerializer(), _mockOutputBindingsInfoProvider.Object,
|
||||||
|
_mockInputConversionFeatureProvider.Object, _testLogger);
|
||||||
|
|
||||||
|
_ = await handler.InvokeAsync(request);
|
||||||
|
|
||||||
|
var result = handler.TryCancel(invocationId);
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
|
|
||||||
public static TestLogger<T> Create()
|
public static TestLogger<T> Create()
|
||||||
{
|
{
|
||||||
// We want to use the logic for category naming which is internal to LoggerFactory.
|
// We want to use the logic for category naming which is internal to LoggerFactory.
|
||||||
// So we'll create a TestLogger via the LoggerFactory and grab it's category.
|
// So we'll create a TestLogger via the LoggerFactory and grab it's category.
|
||||||
TestLoggerProvider testLoggerProvider = new TestLoggerProvider();
|
TestLoggerProvider testLoggerProvider = new TestLoggerProvider();
|
||||||
LoggerFactory _testLoggerFactory = new LoggerFactory(new[] { testLoggerProvider });
|
LoggerFactory _testLoggerFactory = new LoggerFactory(new[] { testLoggerProvider });
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Azure.Functions.Worker.Context.Features;
|
using Microsoft.Azure.Functions.Worker.Context.Features;
|
||||||
using Microsoft.Azure.Functions.Worker.OutputBindings;
|
using Microsoft.Azure.Functions.Worker.OutputBindings;
|
||||||
|
@ -14,7 +15,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
internal class TestAsyncFunctionContext : TestFunctionContext, IAsyncDisposable
|
internal class TestAsyncFunctionContext : TestFunctionContext, IAsyncDisposable
|
||||||
{
|
{
|
||||||
public TestAsyncFunctionContext()
|
public TestAsyncFunctionContext()
|
||||||
: base(new TestFunctionDefinition(), new TestFunctionInvocation())
|
: base(new TestFunctionDefinition(), new TestFunctionInvocation(), CancellationToken.None)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
public TestAsyncFunctionContext(IInvocationFeatures features) : base(features)
|
public TestAsyncFunctionContext(IInvocationFeatures features) : base(features)
|
||||||
|
@ -33,21 +34,33 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
internal class TestFunctionContext : FunctionContext, IDisposable
|
internal class TestFunctionContext : FunctionContext, IDisposable
|
||||||
{
|
{
|
||||||
private readonly FunctionInvocation _invocation;
|
private readonly FunctionInvocation _invocation;
|
||||||
|
private readonly CancellationToken _cancellationToken;
|
||||||
|
|
||||||
public TestFunctionContext()
|
public TestFunctionContext()
|
||||||
: this(new TestFunctionDefinition(), new TestFunctionInvocation())
|
: this(new TestFunctionDefinition(), new TestFunctionInvocation(), CancellationToken.None)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestFunctionContext(IInvocationFeatures features)
|
public TestFunctionContext(IInvocationFeatures features)
|
||||||
: this(new TestFunctionDefinition(), new TestFunctionInvocation(), features)
|
: this(new TestFunctionDefinition(), new TestFunctionInvocation(), CancellationToken.None, features)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestFunctionContext(FunctionDefinition functionDefinition, FunctionInvocation invocation, IInvocationFeatures features = null, IServiceProvider serviceProvider = null)
|
public TestFunctionContext(IInvocationFeatures features, CancellationToken token)
|
||||||
|
: this(new TestFunctionDefinition(), new TestFunctionInvocation(), token, features)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestFunctionContext(FunctionDefinition functionDefinition, FunctionInvocation invocation)
|
||||||
|
: this(functionDefinition, invocation, CancellationToken.None)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestFunctionContext(FunctionDefinition functionDefinition, FunctionInvocation invocation, CancellationToken cancellationToken, IInvocationFeatures features = null, IServiceProvider serviceProvider = null)
|
||||||
{
|
{
|
||||||
FunctionDefinition = functionDefinition;
|
FunctionDefinition = functionDefinition;
|
||||||
_invocation = invocation;
|
_invocation = invocation;
|
||||||
|
_cancellationToken = cancellationToken;
|
||||||
|
|
||||||
if (features != null)
|
if (features != null)
|
||||||
{
|
{
|
||||||
|
@ -86,6 +99,8 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
|
|
||||||
public override RetryContext RetryContext => Features.Get<IExecutionRetryFeature>()?.Context;
|
public override RetryContext RetryContext => Features.Get<IExecutionRetryFeature>()?.Context;
|
||||||
|
|
||||||
|
public override CancellationToken CancellationToken => _cancellationToken;
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
IsDisposed = true;
|
IsDisposed = true;
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
// Licensed under the MIT License. See License.txt in the project root for license information.
|
// Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.Azure.Functions.Worker.Context;
|
|
||||||
|
|
||||||
namespace Microsoft.Azure.Functions.Worker.Tests
|
namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.Azure.Functions.Worker.Context.Features;
|
using Microsoft.Azure.Functions.Worker.Context.Features;
|
||||||
using Microsoft.Azure.Functions.Worker.Converters;
|
using Microsoft.Azure.Functions.Worker.Converters;
|
||||||
|
using Microsoft.Azure.Functions.Worker.Grpc.Messages;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Xunit.Sdk;
|
using Xunit.Sdk;
|
||||||
|
@ -54,5 +55,23 @@ namespace Microsoft.Azure.Functions.Worker.Tests
|
||||||
options ??= new TOptions();
|
options ??= new TOptions();
|
||||||
return new OptionsWrapper<TOptions>(options);
|
return new OptionsWrapper<TOptions>(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static InvocationRequest CreateInvocationRequest(string invocationId = "")
|
||||||
|
{
|
||||||
|
return new InvocationRequest
|
||||||
|
{
|
||||||
|
InvocationId = invocationId,
|
||||||
|
TraceContext = new RpcTraceContext
|
||||||
|
{
|
||||||
|
TraceParent = Guid.NewGuid().ToString(),
|
||||||
|
TraceState = Guid.NewGuid().ToString()
|
||||||
|
},
|
||||||
|
RetryContext = new Grpc.Messages.RetryContext
|
||||||
|
{
|
||||||
|
MaxRetryCount = 3,
|
||||||
|
RetryCount = 2
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,6 +72,32 @@
|
||||||
"maximumInterval": "00:15:00"
|
"maximumInterval": "00:15:00"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "HttpTriggerWithCancellation",
|
||||||
|
"scriptFile": "FunctionApp.dll",
|
||||||
|
"entryPoint": "FunctionApp.HttpTriggerWithCancellation.Run",
|
||||||
|
"language": "dotnet-isolated",
|
||||||
|
"properties": {
|
||||||
|
"IsCodeless": false
|
||||||
|
},
|
||||||
|
"bindings": [
|
||||||
|
{
|
||||||
|
"name": "req",
|
||||||
|
"type": "httpTrigger",
|
||||||
|
"direction": "In",
|
||||||
|
"authLevel": "Anonymous",
|
||||||
|
"methods": [
|
||||||
|
"get",
|
||||||
|
"post"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$return",
|
||||||
|
"type": "http",
|
||||||
|
"direction": "Out"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "HttpTriggerWithDependencyInjection",
|
"name": "HttpTriggerWithDependencyInjection",
|
||||||
"scriptFile": "FunctionApp.dll",
|
"scriptFile": "FunctionApp.dll",
|
||||||
|
|
Загрузка…
Ссылка в новой задаче