Skip to content

Commit fa93167

Browse files
authored
Added Category to ApplicationFailureException (#483)
Fixes #448
1 parent 864e654 commit fa93167

File tree

6 files changed

+57
-8
lines changed

6 files changed

+57
-8
lines changed

src/Temporalio/Converters/DefaultFailureConverter.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ appExc.Details as OutboundFailureDetails
155155
appDet.Count == 0
156156
? null
157157
: new() { Payloads_ = { appDet.Details.Select(conv.ToPayload) } },
158+
Category = appExc.Category,
158159
};
159160
if (appExc.NextRetryDelay != null)
160161
{

src/Temporalio/Exceptions/ApplicationFailureException.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using Temporalio.Api.Enums.V1;
34
using Temporalio.Api.Failure.V1;
45

56
namespace Temporalio.Exceptions
@@ -25,18 +26,21 @@ public class ApplicationFailureException : FailureException
2526
/// <param name="nonRetryable">If true, marks the exception as non-retryable.</param>
2627
/// <param name="details">Collection of details to serialize into the exception.</param>
2728
/// <param name="nextRetryDelay">Override the next retry delay with this value.</param>
29+
/// <param name="category">Error category.</param>
2830
public ApplicationFailureException(
2931
string message,
3032
string? errorType = null,
3133
bool nonRetryable = false,
3234
IReadOnlyCollection<object?>? details = null,
33-
TimeSpan? nextRetryDelay = null)
35+
TimeSpan? nextRetryDelay = null,
36+
ApplicationErrorCategory category = ApplicationErrorCategory.Unspecified)
3437
: base(message)
3538
{
3639
ErrorType = errorType;
3740
NonRetryable = nonRetryable;
3841
Details = new OutboundFailureDetails(details ?? Array.Empty<object?>());
3942
NextRetryDelay = nextRetryDelay;
43+
Category = category;
4044
}
4145

4246
/// <summary>
@@ -50,19 +54,22 @@ public ApplicationFailureException(
5054
/// <param name="nonRetryable">If true, marks the exception as non-retryable.</param>
5155
/// <param name="details">Collection of details to serialize into the exception.</param>
5256
/// <param name="nextRetryDelay">Override the next retry delay with this value.</param>
57+
/// <param name="category">Error category.</param>
5358
public ApplicationFailureException(
5459
string message,
5560
Exception? inner,
5661
string? errorType = null,
5762
bool nonRetryable = false,
5863
IReadOnlyCollection<object?>? details = null,
59-
TimeSpan? nextRetryDelay = null)
64+
TimeSpan? nextRetryDelay = null,
65+
ApplicationErrorCategory category = ApplicationErrorCategory.Unspecified)
6066
: base(message, inner)
6167
{
6268
ErrorType = errorType;
6369
NonRetryable = nonRetryable;
6470
Details = new OutboundFailureDetails(details);
6571
NextRetryDelay = nextRetryDelay;
72+
Category = category;
6673
}
6774

6875
/// <summary>
@@ -84,6 +91,7 @@ internal protected ApplicationFailureException(
8491
NonRetryable = info.NonRetryable;
8592
Details = new InboundFailureDetails(converter, info.Details?.Payloads_);
8693
NextRetryDelay = info.NextRetryDelay?.ToTimeSpan();
94+
Category = info.Category;
8795
}
8896

8997
/// <summary>
@@ -109,5 +117,10 @@ internal protected ApplicationFailureException(
109117
/// Gets the next retry delay override if any was set.
110118
/// </summary>
111119
public TimeSpan? NextRetryDelay { get; protected init; }
120+
121+
/// <summary>
122+
/// Gets the error category.
123+
/// </summary>
124+
public ApplicationErrorCategory Category { get; protected init; }
112125
}
113126
}

src/Temporalio/Worker/ActivityWorker.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Google.Protobuf.WellKnownTypes;
1111
using Microsoft.Extensions.Logging;
1212
using Temporalio.Activities;
13+
using Temporalio.Api.Enums.V1;
1314
using Temporalio.Client;
1415
using Temporalio.Common;
1516
using Temporalio.Converters;
@@ -459,10 +460,22 @@ await Task.WhenAll(tsk.Start.Input.Select(p =>
459460
}
460461
catch (Exception e)
461462
{
462-
act.Context.Logger.LogWarning(
463-
e,
464-
"Completing activity {ActivityType} as failed",
465-
act.Context.Info.ActivityType);
463+
// Downgrade log level to DEBUG for benign application errors.
464+
if ((e as ApplicationFailureException)?.Category == ApplicationErrorCategory.Benign)
465+
{
466+
act.Context.Logger.LogDebug(
467+
e,
468+
"Completing activity {ActivityType} as failed (benign)",
469+
act.Context.Info.ActivityType);
470+
}
471+
else
472+
{
473+
act.Context.Logger.LogWarning(
474+
e,
475+
"Completing activity {ActivityType} as failed",
476+
act.Context.Info.ActivityType);
477+
}
478+
466479
completion.Result.Failed = new()
467480
{
468481
Failure_ = await dataConverter.ToFailureAsync(e).ConfigureAwait(false),

tests/Temporalio.Tests/Client/WorkflowHandleTests.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,23 @@ await Client.ExecuteWorkflowAsync(
4545
Assert.Equal("Some Details", appErr.Details.ElementAt<string>(0));
4646
}
4747

48+
[Fact]
49+
public async Task GetResultAsync_BenignFailure_Throws()
50+
{
51+
var err = await Assert.ThrowsAsync<Exceptions.WorkflowFailedException>(async () =>
52+
{
53+
var arg = new KSWorkflowParams(new KSAction(
54+
Error: new(Message: "Some Message", Details: "Some Details", IsBenign: true)));
55+
await Client.ExecuteWorkflowAsync(
56+
(IKitchenSinkWorkflow wf) => wf.RunAsync(arg),
57+
new(id: $"workflow-{Guid.NewGuid()}", taskQueue: Env.KitchenSinkWorkerTaskQueue));
58+
});
59+
var appErr = Assert.IsType<Exceptions.ApplicationFailureException>(err.InnerException);
60+
Assert.Equal("Some Message", appErr.Message);
61+
Assert.Equal("Some Details", appErr.Details.ElementAt<string>(0));
62+
Assert.Equal(ApplicationErrorCategory.Benign, appErr.Category);
63+
}
64+
4865
[Fact]
4966
public async Task GetResultAsync_NotFoundWorkflow_Throws()
5067
{

tests/Temporalio.Tests/IKitchenSinkWorkflow.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ public record KSResultAction(
6666
public record KSErrorAction(
6767
[property: JsonPropertyName("message")] string? Message = null,
6868
[property: JsonPropertyName("details")] object? Details = null,
69-
[property: JsonPropertyName("attempt")] bool Attempt = false);
69+
[property: JsonPropertyName("attempt")] bool Attempt = false,
70+
[property: JsonPropertyName("is_benign")] bool IsBenign = false);
7071

7172
public record KSContinueAsNewAction(
7273
[property: JsonPropertyName("while_above_zero")] int? WhileAboveZero = null,

tests/Temporalio.Tests/KitchenSinkWorkflow.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Temporalio.Api.Enums.V1;
2+
13
namespace Temporalio.Tests;
24

35
using System.Threading.Tasks;
@@ -76,7 +78,9 @@ public class KitchenSinkWorkflow
7678
details = new[] { action.Error.Details };
7779
}
7880
throw new ApplicationFailureException(
79-
action.Error.Message ?? string.Empty, details: details);
81+
action.Error.Message ?? string.Empty,
82+
details: details,
83+
category: action.Error.IsBenign ? ApplicationErrorCategory.Benign : ApplicationErrorCategory.Unspecified);
8084
}
8185
else if (action.ContinueAsNew != null)
8286
{

0 commit comments

Comments
 (0)