Skip to content

Commit 63d5954

Browse files
Setting current activity status. (#11313)
* Setting current activity status
1 parent 3b49adf commit 63d5954

File tree

5 files changed

+129
-14
lines changed

5 files changed

+129
-14
lines changed

release_notes.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@
99
- Add JitTrace Files for v4.1043 (#11281)
1010
- Update `Microsoft.Azure.WebJobs` reference to `3.0.42` (#11309)
1111
- Adding Activity wrapper to create a function-level request telemetry when none exists (#11311)
12+
- Setting current activity status for failed invocations (#11313)
1213
- Adding test coverage for `Utility.IsAzureMonitorLoggingEnabled` (#11322)
13-
- Reduce allocations in `Utility.IsAzureMonitorLoggingEnabled` (#11323)
14+
- Reduce allocations in `Utility.IsAzureMonitorLoggingEnabled` (#11323)

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

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
@@ -33,7 +33,6 @@
3333
using Microsoft.Azure.WebJobs.Script.Workers.SharedMemoryDataTransfer;
3434
using Microsoft.Extensions.Logging;
3535
using Microsoft.Extensions.Options;
36-
using OpenTelemetry.Trace;
3736
using static Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcLog.Types;
3837
using FunctionMetadata = Microsoft.Azure.WebJobs.Script.Description.FunctionMetadata;
3938
using MsgType = Microsoft.Azure.WebJobs.Script.Grpc.Messages.StreamingMessage.ContentOneofCase;
@@ -859,14 +858,14 @@ internal async Task SendInvocationRequest(ScriptInvocationContext context)
859858
if (_functionLoadErrors.TryGetValue(functionId, out Exception exception))
860859
{
861860
_workerChannelLogger.LogDebug("Function {functionName} failed to load", context.FunctionMetadata.Name);
862-
context.ResultSource.TrySetException(exception);
861+
context.SetException(exception);
863862
RemoveExecutingInvocation(invocationId);
864863
return;
865864
}
866865
else if (_metadataRequestErrors.TryGetValue(functionId, out exception))
867866
{
868867
_workerChannelLogger.LogDebug("Worker failed to load metadata for {functionName}", context.FunctionMetadata.Name);
869-
context.ResultSource.TrySetException(exception);
868+
context.SetException(exception);
870869
RemoveExecutingInvocation(invocationId);
871870
return;
872871
}
@@ -914,7 +913,7 @@ await SendStreamingMessageAsync(new StreamingMessage
914913
}
915914
catch (Exception invokeEx)
916915
{
917-
context.ResultSource.TrySetException(invokeEx);
916+
context.SetException(invokeEx);
918917
}
919918
}
920919

@@ -1132,14 +1131,14 @@ internal async Task InvokeResponse(InvocationResponse invokeResponse)
11321131
else
11331132
{
11341133
var rpcException = invokeResponse.Result.GetRpcException(userCodeExceptionHandlingEnabled);
1135-
context.ResultSource.SetException(rpcException);
1134+
context.SetException(rpcException);
11361135

11371136
_metricsLogger.LogEvent(_workerInvocationFailedMetric);
11381137
}
11391138
}
11401139
catch (Exception exc)
11411140
{
1142-
context.ResultSource.TrySetException(exc);
1141+
context.SetException(exc);
11431142
}
11441143
finally
11451144
{
@@ -1284,8 +1283,6 @@ internal void Log(GrpcEvent msg)
12841283
// TODO fix RpcException catch all https://github.com/Azure/azure-functions-dotnet-worker/issues/370
12851284
var exception = new Workers.Rpc.RpcException(rpcLog.Message, rpcLog.Exception.Message, rpcLog.Exception.StackTrace);
12861285
context.Logger.Log(logLevel, new EventId(0, rpcLog.EventId), rpcLog.Message, exception, (state, exc) => state);
1287-
Activity.Current?.AddException(exception);
1288-
Activity.Current?.SetStatus(ActivityStatusCode.Error, exception.Message);
12891286
}
12901287
else
12911288
{
@@ -1566,7 +1563,7 @@ public bool TryFailExecutions(Exception workerException)
15661563
{
15671564
string invocationId = invocation.Context?.ExecutionContext?.InvocationId.ToString();
15681565
_workerChannelLogger.LogDebug("Worker '{workerId}' encountered a fatal error. Failing invocation: '{invocationId}'", _workerId, invocationId);
1569-
invocation.Context?.ResultSource?.TrySetException(workerException);
1566+
invocation.Context?.SetException(workerException);
15701567
RemoveExecutingInvocation(invocationId);
15711568
}
15721569
return true;

src/WebJobs.Script.Grpc/Extensions/ScriptInvocationContextExtensions.cs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
67
using System.IO;
78
using System.Linq;
89
using System.Text;
@@ -269,5 +270,40 @@ internal static void SetRetryContext(ScriptInvocationContext context, Invocation
269270
}
270271
}
271272
}
273+
274+
public static void SetException(this ScriptInvocationContext context, Exception exception)
275+
{
276+
context.ResultSource?.TrySetException(exception);
277+
278+
// If AsyncExecutionContext is null, skip running.
279+
if (context?.AsyncExecutionContext is null)
280+
{
281+
return;
282+
}
283+
284+
// Restore the execution context from the original invocation. This allows AsyncLocal state to flow to loggers.
285+
System.Threading.ExecutionContext.Run(
286+
context.AsyncExecutionContext,
287+
static state =>
288+
{
289+
var invocationState = (InvocationState)state!;
290+
Activity.Current?.AddException(invocationState.Exception);
291+
Activity.Current?.SetStatus(ActivityStatusCode.Error, invocationState.Exception.Message);
292+
},
293+
new InvocationState(context, exception));
294+
}
295+
296+
private sealed class InvocationState
297+
{
298+
public InvocationState(ScriptInvocationContext context, Exception exception)
299+
{
300+
Context = context;
301+
Exception = exception;
302+
}
303+
304+
public ScriptInvocationContext Context { get; }
305+
306+
public Exception Exception { get; }
307+
}
272308
}
273309
}

test/WebJobs.Script.Tests/Workers/Rpc/GrpcWorkerChannelTests.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
@@ -1561,6 +1561,28 @@ public async Task NullOutputBinding_DoesNotThrow()
15611561
Assert.Equal(TaskStatus.RanToCompletion, resultSource.Task.Status);
15621562
}
15631563

1564+
[Fact]
1565+
public async Task Ensure_Failure_Status_On_CurrentActivity_WhenInvocationFailed()
1566+
{
1567+
await CreateDefaultWorkerChannel(capabilities: new Dictionary<string, string>() { { RpcWorkerConstants.HttpUri, "http://localhost:1234" } });
1568+
Activity activity = new Activity("testActivity");
1569+
activity.Start();
1570+
1571+
var httpInvocationId = Guid.NewGuid();
1572+
ScriptInvocationContext httpInvocationContext = GetTestScriptInvocationContext(httpInvocationId, new TaskCompletionSource<ScriptInvocationResult>(), logger: _logger);
1573+
httpInvocationContext.FunctionMetadata = BuildFunctionMetadataForHttpTrigger("httpTrigger");
1574+
1575+
// Send http trigger invocation invocation request.
1576+
await _workerChannel.SendInvocationRequest(httpInvocationContext);
1577+
1578+
// Send http trigger invocation response
1579+
await _workerChannel.InvokeResponse(BuildFailureInvocationResponse(httpInvocationId.ToString()));
1580+
activity.Stop();
1581+
1582+
Assert.Equal(ActivityStatusCode.Error, activity.Status);
1583+
Assert.Contains("Failure", activity.StatusDescription);
1584+
}
1585+
15641586
private static IEnumerable<FunctionMetadata> GetTestFunctionsList(string runtime, bool addWorkerProperties = false)
15651587
{
15661588
return GetTestFunctionsList(runtime, numberOfFunctions: 2, addWorkerProperties);
@@ -1705,6 +1727,18 @@ private static InvocationResponse BuildSuccessfulInvocationResponse(string invoc
17051727
};
17061728
}
17071729

1730+
private static InvocationResponse BuildFailureInvocationResponse(string invocationId)
1731+
{
1732+
return new InvocationResponse
1733+
{
1734+
InvocationId = invocationId,
1735+
Result = new StatusResult
1736+
{
1737+
Status = StatusResult.Types.Status.Failure
1738+
},
1739+
};
1740+
}
1741+
17081742
private InvocationResponse BuildSuccessfulInvocationResponseWithNullOutputBinding(string invocationId)
17091743
{
17101744
StatusResult statusResult = new StatusResult()

test/WebJobs.Script.Tests/Workers/ScriptInvocationContextExtensionsTests.cs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
67
using System.IO;
78
using System.Linq;
89
using System.Runtime.InteropServices;
@@ -1020,6 +1021,52 @@ public async Task ToRpcInvocationRequest_RpcDataTransfer()
10201021
Assert.Equal(inputBytes, result.InputData[2].Data.Bytes);
10211022
}
10221023

1024+
[Fact]
1025+
public void RecordException_WhenAsyncExecutionContextIsNull_SetsException()
1026+
{
1027+
// Arrange
1028+
var context = new ScriptInvocationContext
1029+
{
1030+
ResultSource = new TaskCompletionSource<ScriptInvocationResult>(),
1031+
AsyncExecutionContext = null
1032+
};
1033+
var ex = new InvalidOperationException("Test error");
1034+
1035+
// Act
1036+
context.SetException(ex);
1037+
1038+
// Assert
1039+
Assert.True(context.ResultSource.Task.IsFaulted);
1040+
Assert.Same(ex, context.ResultSource.Task.Exception!.InnerException);
1041+
}
1042+
1043+
[Fact]
1044+
public void RecordException_WhenAsyncExecutionContextNotNull_SetsExceptionAndUpdatesActivity()
1045+
{
1046+
using var activity = new Activity("TestActivity");
1047+
activity.Start();
1048+
1049+
// Arrange
1050+
var context = new ScriptInvocationContext
1051+
{
1052+
ResultSource = new TaskCompletionSource<ScriptInvocationResult>(),
1053+
AsyncExecutionContext = System.Threading.ExecutionContext.Capture()
1054+
};
1055+
1056+
var ex = new InvalidOperationException("Test error");
1057+
1058+
// Act
1059+
context.SetException(ex);
1060+
1061+
// Assert
1062+
Assert.True(context.ResultSource.Task.IsFaulted);
1063+
Assert.Same(ex, context.ResultSource.Task.Exception!.InnerException);
1064+
1065+
// Verify Activity was updated
1066+
Assert.Equal(ActivityStatusCode.Error, activity.Status);
1067+
Assert.Contains(activity.Events, e => e.Name == "exception");
1068+
}
1069+
10231070
private class TestPoco
10241071
{
10251072
public string Name { get; set; }

0 commit comments

Comments
 (0)