Skip to content

Commit d4032df

Browse files
authored
Improve handling usage errors (#450)
Throw exceptions with user-friendly messages when trying to invoke a non-existing or an incorrect activity function
1 parent 7964ef5 commit d4032df

File tree

5 files changed

+148
-10
lines changed

5 files changed

+148
-10
lines changed

src/Durable/ActivityInvocationTracker.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Durable
1111
using System.Threading;
1212

1313
using Utility;
14+
using WebJobs.Script.Grpc.Messages;
1415

1516
internal class ActivityInvocationTracker
1617
{
@@ -20,9 +21,12 @@ public void ReplayActivityOrStop(
2021
string functionName,
2122
object functionInput,
2223
OrchestrationContext context,
24+
IEnumerable<AzFunctionInfo> loadedFunctions,
2325
bool noWait,
2426
Action<object> output)
2527
{
28+
ValidateActivityFunction(functionName, loadedFunctions);
29+
2630
context.OrchestrationActionCollector.Add(new CallActivityAction(functionName, functionInput));
2731

2832
if (noWait)
@@ -114,5 +118,24 @@ private static object GetEventResult(HistoryEvent historyEvent)
114118
{
115119
return TypeExtensions.ConvertFromJson(historyEvent.Result);
116120
}
121+
122+
private static void ValidateActivityFunction(string functionName, IEnumerable<AzFunctionInfo> loadedFunctions)
123+
{
124+
var functionInfo = loadedFunctions.FirstOrDefault(fi => fi.FuncName == functionName);
125+
if (functionInfo == null)
126+
{
127+
var message = string.Format(PowerShellWorkerStrings.FunctionNotFound, functionName);
128+
throw new InvalidOperationException(message);
129+
}
130+
131+
var activityTriggerBinding = functionInfo.InputBindings.FirstOrDefault(
132+
entry => DurableBindings.IsActivityTrigger(entry.Value.Type)
133+
&& entry.Value.Direction == BindingInfo.Types.Direction.In);
134+
if (activityTriggerBinding.Key == null)
135+
{
136+
var message = string.Format(PowerShellWorkerStrings.FunctionDoesNotHaveProperActivityFunctionBinding, functionName);
137+
throw new InvalidOperationException(message);
138+
}
139+
}
117140
}
118141
}

src/Durable/InvokeActivityFunctionCommand.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ protected override void EndProcessing()
3838
{
3939
var privateData = (Hashtable)MyInvocation.MyCommand.Module.PrivateData;
4040
var context = (OrchestrationContext)privateData[SetFunctionInvocationContextCommand.ContextKey];
41-
_activityInvocationTracker.ReplayActivityOrStop(FunctionName, Input, context, NoWait.IsPresent, WriteObject);
41+
var loadedFunctions = FunctionLoader.GetLoadedFunctions();
42+
_activityInvocationTracker.ReplayActivityOrStop(
43+
FunctionName, Input, context, loadedFunctions, NoWait.IsPresent, WriteObject);
4244
}
4345

4446
protected override void StopProcessing()

src/FunctionInfo.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,16 @@ internal AzFunctionInfo(RpcFunctionMetadata metadata)
145145
OutputBindings = new ReadOnlyDictionary<string, ReadOnlyBindingInfo>(outputBindings);
146146
}
147147

148+
// For testing purposes only
149+
// TODO: Extract a constructor that just takes the field values directly, and move the RpcFunctionMetadata
150+
// parsing logic to a separate constructor or a factory method. When this is done, the new
151+
// simple constructor can be used for testing instead of this one.
152+
internal AzFunctionInfo(string funcName, ReadOnlyDictionary<string, ReadOnlyBindingInfo> inputBindings)
153+
{
154+
FuncName = funcName;
155+
InputBindings = inputBindings;
156+
}
157+
148158
private Dictionary<string, PSScriptParamInfo> GetParameters(string scriptFile, string entryPoint, out ScriptBlockAst scriptAst)
149159
{
150160
scriptAst = Parser.ParseFile(scriptFile, out _, out ParseError[] errors);
@@ -220,9 +230,14 @@ internal PSScriptParamInfo(ParameterAst paramAst)
220230
public class ReadOnlyBindingInfo
221231
{
222232
internal ReadOnlyBindingInfo(BindingInfo bindingInfo)
233+
: this(bindingInfo.Type, bindingInfo.Direction)
234+
{
235+
}
236+
237+
internal ReadOnlyBindingInfo(string type, BindingInfo.Types.Direction direction)
223238
{
224-
Type = bindingInfo.Type;
225-
Direction = bindingInfo.Direction;
239+
Type = type;
240+
Direction = direction;
226241
}
227242

228243
/// <summary>

src/resources/PowerShellWorkerStrings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,4 +316,10 @@
316316
<data name="EnvironmentReloadCompleted" xml:space="preserve">
317317
<value>Environment reload completed in {0} ms.</value>
318318
</data>
319+
<data name="FunctionNotFound" xml:space="preserve">
320+
<value>Function '{0}' is not found.</value>
321+
</data>
322+
<data name="FunctionDoesNotHaveProperActivityFunctionBinding" xml:space="preserve">
323+
<value>Function '{0}' does not have an activityTrigger input binding.</value>
324+
</data>
319325
</root>

test/Unit/Durable/ActivityInvocationTrackerTests.cs

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Test.Durable
1212
using System.Linq;
1313
using System.Threading;
1414
using Microsoft.Azure.Functions.PowerShellWorker.Durable;
15+
using WebJobs.Script.Grpc.Messages;
1516
using Xunit;
1617

1718
public class ActivityInvocationTrackerTests
@@ -21,6 +22,11 @@ public class ActivityInvocationTrackerTests
2122
private const string InvocationResult = "Invocation result";
2223
private const string InvocationResultJson = "\"Invocation result\"";
2324

25+
private const string ActivityTriggerBindingType = "activityTrigger";
26+
27+
private readonly IEnumerable<AzFunctionInfo> _loadedFunctions =
28+
new[] { CreateFakeActivityTriggerAzFunctionInfo(FunctionName) };
29+
2430
private int _nextEventId = 1;
2531

2632
[Theory]
@@ -33,8 +39,9 @@ public void ReplayActivityOrStop_ReplaysActivity_IfActivityCompleted(
3339
var allOutput = new List<object>();
3440

3541
var activityInvocationTracker = new ActivityInvocationTracker();
36-
activityInvocationTracker.ReplayActivityOrStop(FunctionName, FunctionInput, orchestrationContext, noWait: false,
37-
output => { allOutput.Add(output); });
42+
activityInvocationTracker.ReplayActivityOrStop(
43+
FunctionName, FunctionInput, orchestrationContext, _loadedFunctions, noWait: false,
44+
output => { allOutput.Add(output); });
3845

3946
VerifyCallActivityActionAdded(orchestrationContext);
4047
Assert.Equal(InvocationResult, allOutput.Single());
@@ -53,8 +60,9 @@ public void ReplayActivityOrStop_OutputsNothing_IfActivityNotCompleted(
5360
var activityInvocationTracker = new ActivityInvocationTracker();
5461
EmulateStop(activityInvocationTracker);
5562

56-
activityInvocationTracker.ReplayActivityOrStop(FunctionName, FunctionInput, orchestrationContext, noWait: false,
57-
_ => { Assert.True(false, "Unexpected output"); });
63+
activityInvocationTracker.ReplayActivityOrStop(
64+
FunctionName, FunctionInput, orchestrationContext, _loadedFunctions, noWait: false,
65+
_ => { Assert.True(false, "Unexpected output"); });
5866

5967
VerifyCallActivityActionAdded(orchestrationContext);
6068
}
@@ -77,13 +85,19 @@ public void ReplayActivityOrStop_WaitsForStop_IfActivityNotCompleted(bool schedu
7785
() =>
7886
{
7987
activityInvocationTracker.ReplayActivityOrStop(
80-
FunctionName, FunctionInput, orchestrationContext, noWait: false, _ => { });
88+
FunctionName, FunctionInput, orchestrationContext, _loadedFunctions, noWait: false, _ => { });
8189
});
8290
}
8391

8492
[Fact]
8593
public void ReplayActivityOrStop_ReplaysMultipleActivitiesWithTheSameName()
8694
{
95+
var loadedFunctions = new[]
96+
{
97+
CreateFakeActivityTriggerAzFunctionInfo("FunctionA"),
98+
CreateFakeActivityTriggerAzFunctionInfo("FunctionB")
99+
};
100+
87101
var history = MergeHistories(
88102
CreateHistory("FunctionA", scheduled: true, completed: true, output: "\"Result1\""),
89103
CreateHistory("FunctionB", scheduled: true, completed: true, output: "\"Result2\""),
@@ -98,7 +112,7 @@ public void ReplayActivityOrStop_ReplaysMultipleActivitiesWithTheSameName()
98112
for (var i = 0; i < 2; ++i)
99113
{
100114
activityInvocationTracker.ReplayActivityOrStop(
101-
"FunctionA", FunctionInput, orchestrationContext, noWait: false,
115+
"FunctionA", FunctionInput, orchestrationContext, loadedFunctions, noWait: false,
102116
output => { allOutput.Add(output); });
103117
}
104118

@@ -120,13 +134,68 @@ public void ReplayActivityOrStop_OutputsActivityTask_WhenNoWaitRequested(
120134

121135
var activityInvocationTracker = new ActivityInvocationTracker();
122136
activityInvocationTracker.ReplayActivityOrStop(
123-
FunctionName, FunctionInput, orchestrationContext, noWait: true,
137+
FunctionName, FunctionInput, orchestrationContext, _loadedFunctions, noWait: true,
124138
output => { allOutput.Add((ActivityInvocationTask)output); });
125139

126140
VerifyCallActivityActionAdded(orchestrationContext);
127141
Assert.Equal(FunctionName, allOutput.Single().Name);
128142
}
129143

144+
[Fact]
145+
public void ReplayActivityOrStop_Throws_WhenActivityFunctionDoesNotExist()
146+
{
147+
var history = CreateHistory(scheduled: false, completed: false, output: InvocationResultJson);
148+
var orchestrationContext = new OrchestrationContext { History = history };
149+
150+
var loadedFunctions = new[]
151+
{
152+
CreateFakeAzFunctionInfo(FunctionName, "fakeTriggerBindingName", ActivityTriggerBindingType, BindingInfo.Types.Direction.In)
153+
};
154+
155+
const string wrongFunctionName = "AnotherFunction";
156+
157+
var activityInvocationTracker = new ActivityInvocationTracker();
158+
159+
var exception =
160+
Assert.Throws<InvalidOperationException>(
161+
() => activityInvocationTracker.ReplayActivityOrStop(
162+
wrongFunctionName, FunctionInput, orchestrationContext, loadedFunctions, noWait: false,
163+
_ => { Assert.True(false, "Unexpected output"); }));
164+
165+
Assert.Contains(wrongFunctionName, exception.Message);
166+
Assert.DoesNotContain(ActivityTriggerBindingType, exception.Message);
167+
168+
VerifyNoCallActivityActionAdded(orchestrationContext);
169+
}
170+
171+
[Theory]
172+
[InlineData("IncorrectBindingType", BindingInfo.Types.Direction.In)]
173+
[InlineData(ActivityTriggerBindingType, BindingInfo.Types.Direction.Out)]
174+
public void ReplayActivityOrStop_Throws_WhenActivityFunctionHasNoProperBinding(
175+
string bindingType, BindingInfo.Types.Direction bindingDirection)
176+
{
177+
var history = CreateHistory(scheduled: false, completed: false, output: InvocationResultJson);
178+
var orchestrationContext = new OrchestrationContext { History = history };
179+
180+
var loadedFunctions = new[]
181+
{
182+
CreateFakeAzFunctionInfo(FunctionName, "fakeTriggerBindingName", bindingType, bindingDirection)
183+
};
184+
185+
var activityInvocationTracker = new ActivityInvocationTracker();
186+
187+
var exception =
188+
Assert.Throws<InvalidOperationException>(
189+
() => activityInvocationTracker.ReplayActivityOrStop(
190+
FunctionName, FunctionInput, orchestrationContext, loadedFunctions, noWait: false,
191+
_ => { Assert.True(false, "Unexpected output"); }));
192+
193+
Assert.Contains(FunctionName, exception.Message);
194+
Assert.Contains(ActivityTriggerBindingType, exception.Message);
195+
196+
VerifyNoCallActivityActionAdded(orchestrationContext);
197+
}
198+
130199
[Theory]
131200
[InlineData(true, true)]
132201
public void WaitForActivityTasks_OutputsActivityResults_WhenAllTasksCompleted(
@@ -220,6 +289,29 @@ public void WaitForActivityTasks_WaitsForStop_WhenAnyTaskIsNotCompleted(bool sch
220289
});
221290
}
222291

292+
private static AzFunctionInfo CreateFakeActivityTriggerAzFunctionInfo(string functionName)
293+
{
294+
return CreateFakeAzFunctionInfo(functionName, "fakeTriggerBindingName", ActivityTriggerBindingType, BindingInfo.Types.Direction.In);
295+
}
296+
297+
private static AzFunctionInfo CreateFakeAzFunctionInfo(
298+
string functionName,
299+
string bindingName,
300+
string bindingType,
301+
BindingInfo.Types.Direction bindingDirection)
302+
{
303+
return new AzFunctionInfo(
304+
functionName,
305+
new ReadOnlyDictionary<string, ReadOnlyBindingInfo>(
306+
new Dictionary<string, ReadOnlyBindingInfo>
307+
{
308+
{
309+
bindingName,
310+
new ReadOnlyBindingInfo(bindingType, bindingDirection)
311+
}
312+
}));
313+
}
314+
223315
private HistoryEvent[] CreateHistory(bool scheduled, bool completed, string output)
224316
{
225317
return CreateHistory(FunctionName, scheduled, completed, output);

0 commit comments

Comments
 (0)