diff --git a/sample/NodeRetry/HttpTrigger-RetryFunctionJson/index.js b/sample/NodeRetry/HttpTrigger-RetryFunctionJson/index.js index 795819d407..4bee6f6b8c 100644 --- a/sample/NodeRetry/HttpTrigger-RetryFunctionJson/index.js +++ b/sample/NodeRetry/HttpTrigger-RetryFunctionJson/index.js @@ -1,18 +1,27 @@ var invocationCount = 0; +var errorString = 'An error occurred'; module.exports = async function (context, req) { - const reset = req.query.reset; - invocationCount = reset ? 0 : invocationCount + if (context.executionContext.retryContext && (context.executionContext.retryContext.retryCount !== invocationCount + || !(context.executionContext.retryContext.maxRetryCount === 4 || context.executionContext.retryContext.maxRetryCount === 0) + || !(context.executionContext.retryContext.exception.message.includes(errorString)))) { + context.res = { + status: 500 + }; + } else { + const reset = req.query.reset; + invocationCount = reset ? 0 : invocationCount - context.log('JavaScript HTTP trigger function processed a request.invocationCount: ' + invocationCount); + context.log('JavaScript HTTP trigger function processed a request.invocationCount: ' + invocationCount); - invocationCount = invocationCount + 1; - const responseMessage = "invocationCount: " + invocationCount; - if (invocationCount < 4) { - throw new Error('An error occurred'); + invocationCount = invocationCount + 1; + const responseMessage = "invocationCount: " + invocationCount; + if (invocationCount < 4) { + throw new Error(errorString); + } + context.res = { + // status: 200, /* Defaults to 200 */ + body: responseMessage + }; } - context.res = { - // status: 200, /* Defaults to 200 */ - body: responseMessage - }; } \ No newline at end of file diff --git a/sample/NodeRetry/HttpTrigger-RetryHostJson/index.js b/sample/NodeRetry/HttpTrigger-RetryHostJson/index.js index bf4360e5aa..52f209a282 100644 --- a/sample/NodeRetry/HttpTrigger-RetryHostJson/index.js +++ b/sample/NodeRetry/HttpTrigger-RetryHostJson/index.js @@ -1,18 +1,28 @@ var invocationCount = 0; +var errorString = 'An error occurred'; module.exports = async function (context, req) { - const reset = req.query.reset; - invocationCount = reset ? 0 : invocationCount + if (context.executionContext.retryContext && (context.executionContext.retryContext.retryCount !== invocationCount + || !(context.executionContext.retryContext.maxRetryCount === 2 || context.executionContext.retryContext.maxRetryCount === 0) + || !(context.executionContext.retryContext.exception.message.includes(errorString)))) { + debugger; + context.res = { + status: 500 + }; + } else { + const reset = req.query.reset; + invocationCount = reset ? 0 : invocationCount - context.log('JavaScript HTTP trigger function processed a request.invocationCount: ' + invocationCount); - - invocationCount = invocationCount + 1; - const responseMessage = "invocationCount: " + invocationCount; - if (invocationCount < 2) { - throw new Error('An error occurred'); + context.log('JavaScript HTTP trigger function processed a request.invocationCount: ' + invocationCount); + + invocationCount = invocationCount + 1; + const responseMessage = "invocationCount: " + invocationCount; + if (invocationCount < 2) { + throw new Error('An error occurred'); + } + context.res = { + // status: 200, /* Defaults to 200 */ + body: responseMessage + }; } - context.res = { - // status: 200, /* Defaults to 200 */ - body: responseMessage - }; } \ No newline at end of file diff --git a/src/WebJobs.Script.Grpc/Extensions/ScriptInvocationContextExtensions.cs b/src/WebJobs.Script.Grpc/Extensions/ScriptInvocationContextExtensions.cs index 14c8a6ef20..ca686cec0b 100644 --- a/src/WebJobs.Script.Grpc/Extensions/ScriptInvocationContextExtensions.cs +++ b/src/WebJobs.Script.Grpc/Extensions/ScriptInvocationContextExtensions.cs @@ -8,12 +8,14 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.Azure.WebJobs.Host.Diagnostics; using Microsoft.Azure.WebJobs.Script.Description; using Microsoft.Azure.WebJobs.Script.Grpc.Extensions; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; using Microsoft.Azure.WebJobs.Script.Workers.SharedMemoryDataTransfer; using Microsoft.Extensions.Logging; +using RpcException = Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcException; namespace Microsoft.Azure.WebJobs.Script.Grpc { @@ -30,6 +32,8 @@ public static async Task ToRpcInvocationRequest(this ScriptIn TraceContext = GetRpcTraceContext(context.Traceparent, context.Tracestate, context.Attributes, logger), }; + SetRetryContext(context, invocationRequest); + var rpcValueCache = new Dictionary(); Dictionary sharedMemValueCache = null; StringBuilder logBuilder = null; @@ -171,5 +175,27 @@ internal static RpcTraceContext GetRpcTraceContext(string activityId, string tra return traceContext; } + + internal static void SetRetryContext(ScriptInvocationContext context, InvocationRequest invocationRequest) + { + if (context.ExecutionContext.RetryContext != null) + { + invocationRequest.RetryContext = new RetryContext() + { + RetryCount = context.ExecutionContext.RetryContext.RetryCount, + MaxRetryCount = context.ExecutionContext.RetryContext.MaxRetryCount + }; + // RetryContext.Exception should not be null, check just in case + if (context.ExecutionContext.RetryContext.Exception != null) + { + invocationRequest.RetryContext.Exception = new RpcException() + { + Message = ExceptionFormatter.GetFormattedException(context.ExecutionContext.RetryContext.Exception), // merge message from InnerException + StackTrace = context.ExecutionContext.RetryContext.Exception.StackTrace ?? string.Empty, + Source = context.ExecutionContext.RetryContext.Exception.Source ?? string.Empty + }; + } + } + } } } diff --git a/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/README.md b/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/README.md index a307acfcc1..14c406e2ed 100644 --- a/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/README.md +++ b/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/README.md @@ -61,7 +61,7 @@ mkdir %MSGDIR% set OUTDIR=%MSGDIR%\DotNet mkdir %OUTDIR% -%GRPC_TOOLS_PATH%\protoc.exe %PROTO% --csharp_out %OUTDIR% --grpc_out=%OUTDIR% --plugin=protoc-gen-grpc=%GRPC_TOOLS_PATH%\grpc_csharp_plugin.exe --proto_path=%PROTO_PATH% --proto_path=%PROTOBUF_TOOLS% +%GRPC_TOOLS_PATH%\protoc.exe %PROTO% --csharp_out %OUTDIR% --grpc_out=%OUTDIR% --plugin=protoc-gen-grpc=%GRPC_TOOLS_PATH%\grpc_csharp_plugin.exe --proto_path=%PROTO_PATH% --proto_path=%PROTOBUF_TOOLS% ``` ## JavaScript In package.json, add to the build script the following commands to build .js files and to build .ts files. Use and install npm package `protobufjs`. @@ -81,7 +81,10 @@ In pom.xml add following under configuration for this plugin ${basedir}//azure-functions-language-worker-protobuf/src/proto ## Python ---TODO +``` +python -m pip install -e .[dev] -U +python setup.py build +``` ## Contributing diff --git a/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/src/proto/FunctionRpc.proto b/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/src/proto/FunctionRpc.proto index 403156e249..92fc0c73a3 100644 --- a/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/src/proto/FunctionRpc.proto +++ b/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/src/proto/FunctionRpc.proto @@ -278,6 +278,9 @@ message InvocationRequest { // Populates activityId, tracestate and tags from host RpcTraceContext trace_context = 5; + + // Current retry context + RetryContext retry_context = 6; } // Host sends ActivityId, traceStateString and Tags from host @@ -292,6 +295,18 @@ message RpcTraceContext { map attributes = 3; } +// Host sends retry context for a function invocation +message RetryContext { + // Current retry count + int32 retry_count = 1; + + // Max retry count + int32 max_retry_count = 2; + + // Exception that caused the retry + RpcException exception = 3; +} + // Host requests worker to cancel invocation message InvocationCancel { // Unique id for invocation diff --git a/src/WebJobs.Script/WebJobs.Script.csproj b/src/WebJobs.Script/WebJobs.Script.csproj index 48dc48926b..d506156522 100644 --- a/src/WebJobs.Script/WebJobs.Script.csproj +++ b/src/WebJobs.Script/WebJobs.Script.csproj @@ -41,7 +41,7 @@ - + diff --git a/test/WebJobs.Script.Tests.Integration/WebJobs.Script.Tests.Integration.csproj b/test/WebJobs.Script.Tests.Integration/WebJobs.Script.Tests.Integration.csproj index 7e03be530c..221f08e013 100644 --- a/test/WebJobs.Script.Tests.Integration/WebJobs.Script.Tests.Integration.csproj +++ b/test/WebJobs.Script.Tests.Integration/WebJobs.Script.Tests.Integration.csproj @@ -38,7 +38,7 @@ - + diff --git a/test/WebJobs.Script.Tests/Workers/ScriptInvocationContextExtensionsTests.cs b/test/WebJobs.Script.Tests/Workers/ScriptInvocationContextExtensionsTests.cs index 262f3f8493..add13681bd 100644 --- a/test/WebJobs.Script.Tests/Workers/ScriptInvocationContextExtensionsTests.cs +++ b/test/WebJobs.Script.Tests/Workers/ScriptInvocationContextExtensionsTests.cs @@ -307,6 +307,43 @@ public async Task ToRpcInvocationRequest_MultipleInputBindings() Assert.True(result.TriggerMetadata.ContainsKey("query")); } + [Fact] + public void TestSetRetryContext_NoRetry() + { + ScriptInvocationContext context = new ScriptInvocationContext() + { + ExecutionContext = new ExecutionContext() + }; + InvocationRequest request = new InvocationRequest(); + Grpc.ScriptInvocationContextExtensions.SetRetryContext(context, request); + + Assert.Null(request.RetryContext); + } + + [Fact] + public void TestSetRetryContext_Retry() + { + ScriptInvocationContext context = new ScriptInvocationContext() + { + ExecutionContext = new ExecutionContext() + { + RetryContext = new Host.RetryContext() + { + RetryCount = 1, + MaxRetryCount = 2, + Exception = new Exception("test") + } + } + }; + InvocationRequest request = new InvocationRequest(); + Grpc.ScriptInvocationContextExtensions.SetRetryContext(context, request); + + Assert.NotNull(request.RetryContext); + Assert.Equal(request.RetryContext.RetryCount, 1); + Assert.Equal(request.RetryContext.MaxRetryCount, 2); + Assert.NotNull(request.RetryContext.Exception); + } + /// /// The inputs meet the requirement for being transferred over shared memory. /// Ensure that the inputs are converted to .