Skip to content

Commit 91e0d11

Browse files
authored
Send InvocationCancel request to out-of-proc workers (#8556)
1 parent c5dba8a commit 91e0d11

File tree

10 files changed

+205
-31
lines changed

10 files changed

+205
-31
lines changed

release_notes.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
<!-- Please add your release notes in the following format:
33
- My change description (#PR)
44
-->
5+
6+
- Host support for out-of-proc cancellation tokens ([#2153](https://github.com/Azure/azure-functions-host/issues/2152))
57
- Update Python Worker Version to [4.4.0](https://github.com/Azure/azure-functions-python-worker/releases/tag/4.4.0)
8+
69
**Release sprint:** Sprint 125
710
[ [bugs](https://github.com/Azure/azure-functions-host/issues?q=is%3Aissue+milestone%3A%22Functions+Sprint+125%22+label%3Abug+is%3Aclosed) | [features](https://github.com/Azure/azure-functions-host/issues?q=is%3Aissue+milestone%3A%22Functions+Sprint+125%22+label%3Afeature+is%3Aclosed) ]
811
- Fix the bug where debugging of dotnet isolated function apps hangs in visual studio (#8596)

src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ internal class GrpcWorkerChannel : IRpcWorkerChannel, IDisposable
7373
private TaskCompletionSource<List<RawFunctionMetadata>> _functionsIndexingTask = new TaskCompletionSource<List<RawFunctionMetadata>>(TaskCreationOptions.RunContinuationsAsynchronously);
7474
private TimeSpan _functionLoadTimeout = TimeSpan.FromMinutes(1);
7575
private bool _isSharedMemoryDataTransferEnabled;
76+
private bool _cancelCapabilityEnabled;
7677

7778
private object _syncLock = new object();
7879
private System.Timers.Timer _timer;
@@ -275,6 +276,7 @@ internal void WorkerInitResponse(GrpcEvent initEvent)
275276
_state = _state | RpcWorkerChannelState.Initialized;
276277
_workerCapabilities.UpdateCapabilities(_initMessage.Capabilities);
277278
_isSharedMemoryDataTransferEnabled = IsSharedMemoryDataTransferEnabled();
279+
_cancelCapabilityEnabled = !string.IsNullOrEmpty(_workerCapabilities.GetCapabilityState(RpcWorkerConstants.HandlesInvocationCancelMessage));
278280

279281
if (!_isSharedMemoryDataTransferEnabled)
280282
{
@@ -501,36 +503,58 @@ internal async Task SendInvocationRequest(ScriptInvocationContext context)
501503
_workerChannelLogger.LogDebug($"Function {context.FunctionMetadata.Name} failed to load");
502504
context.ResultSource.TrySetException(_functionLoadErrors[context.FunctionMetadata.GetFunctionId()]);
503505
_executingInvocations.TryRemove(context.ExecutionContext.InvocationId.ToString(), out ScriptInvocationContext _);
506+
return;
504507
}
505508
else if (_metadataRequestErrors.ContainsKey(context.FunctionMetadata.GetFunctionId()))
506509
{
507510
_workerChannelLogger.LogDebug($"Worker failed to load metadata for {context.FunctionMetadata.Name}");
508511
context.ResultSource.TrySetException(_metadataRequestErrors[context.FunctionMetadata.GetFunctionId()]);
509512
_executingInvocations.TryRemove(context.ExecutionContext.InvocationId.ToString(), out ScriptInvocationContext _);
513+
return;
510514
}
511-
else
515+
516+
if (context.CancellationToken.IsCancellationRequested)
512517
{
513-
if (context.CancellationToken.IsCancellationRequested)
514-
{
515-
context.ResultSource.SetCanceled();
516-
return;
517-
}
518-
var invocationRequest = await context.ToRpcInvocationRequest(_workerChannelLogger, _workerCapabilities, _isSharedMemoryDataTransferEnabled, _sharedMemoryManager);
519-
AddAdditionalTraceContext(invocationRequest.TraceContext.Attributes, context);
520-
_executingInvocations.TryAdd(invocationRequest.InvocationId, context);
518+
_workerChannelLogger.LogDebug("Cancellation has been requested, cancelling invocation request");
519+
context.ResultSource.SetCanceled();
520+
return;
521+
}
521522

522-
SendStreamingMessage(new StreamingMessage
523-
{
524-
InvocationRequest = invocationRequest
525-
});
523+
var invocationRequest = await context.ToRpcInvocationRequest(_workerChannelLogger, _workerCapabilities, _isSharedMemoryDataTransferEnabled, _sharedMemoryManager);
524+
AddAdditionalTraceContext(invocationRequest.TraceContext.Attributes, context);
525+
_executingInvocations.TryAdd(invocationRequest.InvocationId, context);
526+
527+
if (_cancelCapabilityEnabled)
528+
{
529+
context.CancellationToken.Register(() => SendInvocationCancel(invocationRequest.InvocationId));
526530
}
531+
532+
SendStreamingMessage(new StreamingMessage
533+
{
534+
InvocationRequest = invocationRequest
535+
});
527536
}
528537
catch (Exception invokeEx)
529538
{
530539
context.ResultSource.TrySetException(invokeEx);
531540
}
532541
}
533542

543+
internal void SendInvocationCancel(string invocationId)
544+
{
545+
_workerChannelLogger.LogDebug($"Sending invocation cancel request for InvocationId {invocationId}");
546+
547+
var invocationCancel = new InvocationCancel
548+
{
549+
InvocationId = invocationId
550+
};
551+
552+
SendStreamingMessage(new StreamingMessage
553+
{
554+
InvocationCancel = invocationCancel
555+
});
556+
}
557+
534558
// gets metadata from worker
535559
public Task<List<RawFunctionMetadata>> GetFunctionMetadata()
536560
{

src/WebJobs.Script.Grpc/MessageExtensions/StatusResultExtensions.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,19 @@ public static bool IsFailure(this StatusResult statusResult, out Exception excep
3030
}
3131

3232
/// <summary>
33-
/// This method is only hit on the invocation code path. enableUserCodeExceptionCapability = feature flag,
34-
/// exposed as a capability that is set by the worker.
33+
/// This method is only hit on the invocation code path.
34+
/// enableUserCodeExceptionCapability = feature flag exposed as a capability that is set by the worker.
3535
/// </summary>
3636
public static bool IsInvocationSuccess<T>(this StatusResult status, TaskCompletionSource<T> tcs, bool enableUserCodeExceptionCapability = false)
3737
{
3838
switch (status.Status)
3939
{
4040
case StatusResult.Types.Status.Failure:
41+
case StatusResult.Types.Status.Cancelled:
4142
var rpcException = GetRpcException(status, enableUserCodeExceptionCapability);
4243
tcs.SetException(rpcException);
4344
return false;
4445

45-
case StatusResult.Types.Status.Cancelled:
46-
tcs.SetCanceled();
47-
return false;
48-
4946
default:
5047
return true;
5148
}
@@ -65,6 +62,7 @@ public static Workers.Rpc.RpcException GetRpcException(StatusResult statusResult
6562
{
6663
return new Workers.Rpc.RpcException(status, ex.Message, ex.StackTrace, ex.Type, ex.IsUserException);
6764
}
65+
6866
return new Workers.Rpc.RpcException(status, ex.Message, ex.StackTrace);
6967
}
7068
return new Workers.Rpc.RpcException(status, string.Empty, string.Empty);

src/WebJobs.Script.WebHost/WebScriptHostExceptionHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public async Task OnTimeoutExceptionAsync(ExceptionDispatchInfo exceptionInfo, T
3131

3232
if (timeoutException?.Task != null)
3333
{
34-
// We may double the timeoutGracePeriod here by first waiting to see if the iniital
34+
// We may double the timeoutGracePeriod here by first waiting to see if the initial
3535
// function task that started the exception has completed.
3636
Task completedTask = await Task.WhenAny(timeoutException.Task, Task.Delay(timeoutGracePeriod));
3737

src/WebJobs.Script/Description/Workers/WorkerFunctionDescriptorProvider.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Linq;
88
using System.Reflection;
99
using System.Reflection.Emit;
10+
using System.Threading;
1011
using System.Threading.Tasks;
1112
using Microsoft.AspNetCore.Hosting;
1213
using Microsoft.Azure.WebJobs.Description;
@@ -54,6 +55,9 @@ protected override async Task<Collection<ParameterDescriptor>> GetFunctionParame
5455
{
5556
var parameters = await base.GetFunctionParametersAsync(functionInvoker, functionMetadata, triggerMetadata, methodAttributes, inputBindings, outputBindings);
5657

58+
// Add cancellation token
59+
parameters.Add(new ParameterDescriptor(ScriptConstants.SystemCancellationTokenParameterName, typeof(CancellationToken)));
60+
5761
var bindings = inputBindings.Union(outputBindings);
5862

5963
try

src/WebJobs.Script/Description/Workers/WorkerFunctionInvoker.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,10 @@ protected override async Task<object> InvokeCore(object[] parameters, FunctionIn
6363
await DelayUntilFunctionDispatcherInitializedOrShutdown();
6464
}
6565

66+
var triggerParameterIndex = 0;
67+
var cancellationTokenParameterIndex = 4;
6668
var bindingData = context.Binder.BindingData;
67-
object triggerValue = TransformInput(parameters[0], bindingData);
69+
object triggerValue = TransformInput(parameters[triggerParameterIndex], bindingData);
6870
var triggerInput = (_bindingMetadata.Name, _bindingMetadata.DataType ?? DataType.String, triggerValue);
6971
IEnumerable<(string, DataType, object)> inputs = new[] { triggerInput };
7072
if (_inputBindings.Count > 1)
@@ -84,9 +86,7 @@ protected override async Task<object> InvokeCore(object[] parameters, FunctionIn
8486
Traceparent = Activity.Current?.Id,
8587
Tracestate = Activity.Current?.TraceStateString,
8688
Attributes = Activity.Current?.Tags,
87-
88-
// TODO: link up cancellation token to parameter descriptors
89-
CancellationToken = CancellationToken.None,
89+
CancellationToken = HandleCancellationTokenParameter(parameters[cancellationTokenParameterIndex]),
9090
Logger = context.Logger
9191
};
9292

@@ -187,6 +187,16 @@ private object TransformInput(object input, Dictionary<string, object> bindingDa
187187
return input;
188188
}
189189

190+
private CancellationToken HandleCancellationTokenParameter(object input)
191+
{
192+
if (input == null)
193+
{
194+
return CancellationToken.None;
195+
}
196+
197+
return (CancellationToken)input;
198+
}
199+
190200
private void HandleReturnParameter(ScriptInvocationResult result)
191201
{
192202
result.Outputs[ScriptConstants.SystemReturnParameterBindingName] = result.Return;

src/WebJobs.Script/Host/ScriptHost.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ public async Task InitializeAsync(CancellationToken cancellationToken = default)
317317
{
318318
// Initialize worker function invocation dispatcher only for valid functions after creating function descriptors
319319
// Dispatcher not needed for codeless function.
320-
// Disptacher needed for non-dotnet codeless functions
320+
// Dispatcher needed for non-dotnet codeless functions
321321
var filteredFunctionMetadata = functionMetadataList.Where(m => !Utility.IsCodelessDotNetLanguageFunction(m));
322322
await _functionDispatcher.InitializeAsync(Utility.GetValidFunctions(filteredFunctionMetadata, Functions), cancellationToken);
323323
}

src/WebJobs.Script/ScriptConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public static class ScriptConstants
6666
public const string SystemReturnParameterBindingName = "$return";
6767
public const string SystemReturnParameterName = "_return";
6868
public const string SystemLoggerParameterName = "_logger";
69+
public const string SystemCancellationTokenParameterName = "_cancellationToken";
6970

7071
public const string DebugSentinelFileName = "debug_sentinel";
7172
public const string DiagnosticSentinelFileName = "diagnostic_sentinel";

src/WebJobs.Script/Workers/Rpc/RpcWorkerConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public static class RpcWorkerConstants
5353
public const string EnableUserCodeException = "EnableUserCodeException";
5454
public const string SupportsLoadResponseCollection = "SupportsLoadResponseCollection";
5555
public const string HandlesWorkerTerminateMessage = "HandlesWorkerTerminateMessage";
56+
public const string HandlesInvocationCancelMessage = "HandlesInvocationCancelMessage";
5657

5758
// Host Capabilities
5859
public const string V2Compatable = "V2Compatable";

0 commit comments

Comments
 (0)