Skip to content

Commit 0d4b240

Browse files
committed
Deal with reserved __temporal prefix
1 parent 139a853 commit 0d4b240

File tree

7 files changed

+167
-8
lines changed

7 files changed

+167
-8
lines changed

.editorconfig

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ dotnet_diagnostic.CA2237.severity = none
4747
# Warn on unused imports
4848
dotnet_diagnostic.IDE0005.severity = warning
4949

50+
# Don't warn on using var
51+
dotnet_diagnostic.IDE0008.severity = none
52+
5053
# Cannot use range operator on older versions
5154
dotnet_diagnostic.IDE0057.severity = none
5255

@@ -186,4 +189,4 @@ dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggest
186189
#Style - Pattern matching
187190

188191
#prefer pattern matching instead of is expression with type casts
189-
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
192+
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion

omnisharp.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
{
22
"FormattingOptions": {
33
"EnableEditorConfigSupport": true
4+
},
5+
"RoslynExtensionsOptions": {
6+
"enableAnalyzersSupport": true
47
}
58
}

src/Temporalio/Worker/WorkflowInstance.cs

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public WorkflowInstance(WorkflowInstanceDetails details)
107107
(WorkflowInboundInterceptor)rootInbound,
108108
(v, impl) => impl.InterceptWorkflow(v));
109109
ret.Init(new OutboundImpl(this));
110-
return ret;
110+
return new InboundReservedPrefixInterceptor(rootInbound, ret);
111111
},
112112
false);
113113
outbound = new(
@@ -939,7 +939,11 @@ private Task ApplyDoUpdateAsync(DoUpdate update)
939939
var updates = mutableUpdates.IsValueCreated ? mutableUpdates.Value : Definition.Updates;
940940
if (!updates.TryGetValue(update.Name, out var updateDefn))
941941
{
942-
updateDefn = DynamicUpdate;
942+
// Do not fall back onto dynamic update if using the reserved prefix
943+
if (!update.Name.StartsWith(Workflow.ReservedHandlerPrefix))
944+
{
945+
updateDefn = DynamicUpdate;
946+
}
943947
if (updateDefn == null)
944948
{
945949
var knownUpdates = updates.Keys.OrderBy(k => k);
@@ -1155,7 +1159,11 @@ private void ApplyQueryWorkflow(QueryWorkflow query)
11551159
var queries = mutableQueries.IsValueCreated ? mutableQueries.Value : Definition.Queries;
11561160
if (!queries.TryGetValue(query.QueryType, out queryDefn))
11571161
{
1158-
queryDefn = DynamicQuery;
1162+
// Do not fall back onto dynamic query if using the reserved prefix
1163+
if (!query.QueryType.StartsWith(Workflow.ReservedHandlerPrefix))
1164+
{
1165+
queryDefn = DynamicQuery;
1166+
}
11591167
if (queryDefn == null)
11601168
{
11611169
var knownQueries = queries.Keys.OrderBy(k => k);
@@ -1267,7 +1275,11 @@ private void ApplySignalWorkflow(SignalWorkflow signal)
12671275
var signals = mutableSignals.IsValueCreated ? mutableSignals.Value : Definition.Signals;
12681276
if (!signals.TryGetValue(signal.SignalName, out var signalDefn))
12691277
{
1270-
signalDefn = DynamicSignal;
1278+
// Do not fall back onto dynamic signal if using the reserved prefix
1279+
if (!signal.SignalName.StartsWith(Workflow.ReservedHandlerPrefix))
1280+
{
1281+
signalDefn = DynamicSignal;
1282+
}
12711283
if (signalDefn == null)
12721284
{
12731285
// No definition found, buffer
@@ -2450,5 +2462,51 @@ public record Handler(
24502462
string? UpdateId,
24512463
HandlerUnfinishedPolicy UnfinishedPolicy);
24522464
}
2465+
2466+
/// <summary>
2467+
/// This interceptor ensures that handlers using the reserved prefix bypass user
2468+
/// interceptors.
2469+
/// </summary>
2470+
private class InboundReservedPrefixInterceptor : WorkflowInboundInterceptor
2471+
{
2472+
private readonly InboundImpl root;
2473+
private readonly WorkflowInboundInterceptor complete;
2474+
2475+
public InboundReservedPrefixInterceptor(InboundImpl root, WorkflowInboundInterceptor complete)
2476+
{
2477+
this.root = root;
2478+
this.complete = complete;
2479+
}
2480+
2481+
public override void Init(WorkflowOutboundInterceptor outbound) => complete.Init(outbound);
2482+
2483+
public override Task<object?> ExecuteWorkflowAsync(ExecuteWorkflowInput input) =>
2484+
complete.ExecuteWorkflowAsync(input);
2485+
2486+
public override Task HandleSignalAsync(HandleSignalInput input) =>
2487+
input.Signal.StartsWith(Workflow.ReservedHandlerPrefix)
2488+
? root.HandleSignalAsync(input)
2489+
: complete.HandleSignalAsync(input);
2490+
2491+
public override object? HandleQuery(HandleQueryInput input) =>
2492+
input.Query.StartsWith(Workflow.ReservedHandlerPrefix) ?
2493+
root.HandleQuery(input) :
2494+
complete.HandleQuery(input);
2495+
2496+
public override void ValidateUpdate(HandleUpdateInput input)
2497+
{
2498+
if (input.Update.StartsWith(Workflow.ReservedHandlerPrefix))
2499+
{
2500+
root.ValidateUpdate(input);
2501+
return;
2502+
}
2503+
complete.ValidateUpdate(input);
2504+
}
2505+
2506+
public override Task<object?> HandleUpdateAsync(HandleUpdateInput input) =>
2507+
input.Update.StartsWith(Workflow.ReservedHandlerPrefix) ?
2508+
root.HandleUpdateAsync(input) :
2509+
complete.HandleUpdateAsync(input);
2510+
}
24532511
}
24542512
}

src/Temporalio/Workflows/Workflow.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,21 @@ namespace Temporalio.Workflows
1818
/// </summary>
1919
public static class Workflow
2020
{
21+
/// <summary>
22+
/// Prefix for reserved handler names.
23+
/// </summary>
24+
internal const string ReservedHandlerPrefix = "__temporal";
25+
26+
/// <summary>
27+
/// All known reserved query handler prefixes.
28+
/// </summary>
29+
internal static readonly string[] ReservedQueryHandlerPrefixes =
30+
{
31+
ReservedHandlerPrefix,
32+
"__stack_trace",
33+
"__enhanced_stack_trace",
34+
};
35+
2136
/// <summary>
2237
/// Gets a value indicating whether all update and signal handlers have finished executing.
2338
/// </summary>
@@ -1339,4 +1354,4 @@ public static class Unsafe
13391354
public static bool IsReplaying => Context.IsReplaying;
13401355
}
13411356
}
1342-
}
1357+
}

src/Temporalio/Workflows/WorkflowDefinition.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,35 @@ public static WorkflowDefinition Create(
427427
}
428428
}
429429

430+
// Verify that registered names to not use our reserved prefix
431+
if (name != null && name.StartsWith(Workflow.ReservedHandlerPrefix))
432+
{
433+
errs.Add($"Workflow name {name} cannot start with {Workflow.ReservedHandlerPrefix}");
434+
}
435+
void CheckNamesForPrefix(IEnumerable<string> keys, string type)
436+
{
437+
foreach (var key in keys)
438+
{
439+
if (key.StartsWith(Workflow.ReservedHandlerPrefix))
440+
{
441+
errs.Add($"{type} handler name {key} cannot start with {Workflow.ReservedHandlerPrefix}");
442+
}
443+
else if (type == "Query")
444+
{
445+
foreach (var reservedQ in Workflow.ReservedQueryHandlerPrefixes)
446+
{
447+
if (key.StartsWith(reservedQ))
448+
{
449+
errs.Add($"{type} handler name {key} cannot start with {reservedQ}");
450+
}
451+
}
452+
}
453+
}
454+
}
455+
CheckNamesForPrefix(signals.Keys, "Signal");
456+
CheckNamesForPrefix(queries.Keys, "Query");
457+
CheckNamesForPrefix(updates.Keys, "Update");
458+
430459
// If there are any errors, throw
431460
if (errs.Count > 0)
432461
{
@@ -525,4 +554,4 @@ private static bool IsDefinedOnBase<T>(MethodInfo method)
525554
}
526555
}
527556
}
528-
}
557+
}

tests/Temporalio.Tests/Worker/WorkflowWorkerTests.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2997,6 +2997,17 @@ await ExecuteWorkerAsync<DynamicWorkflow>(
29972997
await handle.SignalAsync(wf => wf.SomeSignalAsync("signal arg"));
29982998
Assert.Equal("done", await handle.QueryAsync(wf => wf.SomeQuery("query arg")));
29992999
Assert.Equal("done", await handle.ExecuteUpdateAsync(wf => wf.SomeUpdateAsync("update arg")));
3000+
3001+
// Verify that invocations using the reserved prefix are not forwarded to dynamic
3002+
// handlers
3003+
await handle.SignalAsync($"{Workflow.ReservedHandlerPrefix}_whatever", new[] { "signal arg 1" });
3004+
var queryExc2 = await Assert.ThrowsAsync<WorkflowQueryFailedException>(
3005+
() => handle.QueryAsync<string>($"{Workflow.ReservedHandlerPrefix}_whatever", new[] { "query arg 1" }));
3006+
Assert.Contains("not found", queryExc2.Message);
3007+
var updateExc = await Assert.ThrowsAsync<WorkflowUpdateFailedException>(
3008+
() => handle.ExecuteUpdateAsync<string>($"{Workflow.ReservedHandlerPrefix}_whatever", new[] { "update arg 1" }));
3009+
Assert.Contains("not found", updateExc.InnerException?.Message);
3010+
30003011
// Event list must be collected before the WF finishes, since when it finishes it
30013012
// will be evicted from the cache and the first SomeQuery event will not exist upon
30023013
// replay.

tests/Temporalio.Tests/Workflows/WorkflowDefinitionTests.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,17 @@ public void Create_BadUpdates_Throws()
234234
AssertBad<Bad.WfUpdate>("ValidateGenericUpdate[TLocal](TLocal) with WorkflowUpdateValidator contains generic parameters");
235235
}
236236

237+
[Fact]
238+
public void Reserved_Handler_Prefixes_Throws()
239+
{
240+
AssertBad<Bad.BadWfName>("Workflow name __temporal_why_would_you_do_this cannot start with __temporal");
241+
AssertBad<Bad.BadHandlerNames>("Signal handler name __temporal_dumb_signal cannot start with __temporal");
242+
AssertBad<Bad.BadHandlerNames>("Query handler name __temporal_dumb_query cannot start with __temporal");
243+
AssertBad<Bad.BadHandlerNames>("Query handler name __stack_trace cannot start with __stack_trace");
244+
AssertBad<Bad.BadHandlerNames>("Query handler name __enhanced_stack_trace cannot start with __enhanced_stack_trace");
245+
AssertBad<Bad.BadHandlerNames>("Update handler name __temporal_dumb_update cannot start with __temporal");
246+
}
247+
237248
private static void AssertBad<T>(string errContains)
238249
{
239250
var err = Assert.ThrowsAny<Exception>(() => WorkflowDefinition.Create(typeof(T)));
@@ -524,6 +535,35 @@ protected void ValidateUpdate5()
524535
{
525536
}
526537
}
538+
539+
[Workflow("__temporal_why_would_you_do_this")]
540+
public class BadWfName
541+
{
542+
[WorkflowRun]
543+
public Task RunAsync() => Task.CompletedTask;
544+
}
545+
546+
[Workflow]
547+
public class BadHandlerNames
548+
{
549+
[WorkflowRun]
550+
public Task RunAsync() => Task.CompletedTask;
551+
552+
[WorkflowSignal("__temporal_dumb_signal")]
553+
public Task SignalAsync() => Task.CompletedTask;
554+
555+
[WorkflowQuery("__temporal_dumb_query")]
556+
public string Query() => string.Empty;
557+
558+
[WorkflowQuery("__stack_trace")]
559+
public string StackTraceImpostor() => string.Empty;
560+
561+
[WorkflowQuery("__enhanced_stack_trace")]
562+
public string EnhancedStackTraceImpostor() => string.Empty;
563+
564+
[WorkflowUpdate("__temporal_dumb_update")]
565+
public Task UpdateAsync() => Task.CompletedTask;
566+
}
527567
}
528568

529569
public static class Good
@@ -583,4 +623,4 @@ public Wf2(string param1, int param2 = 5)
583623
public override Task SomeSignalAsync() => Task.CompletedTask;
584624
}
585625
}
586-
}
626+
}

0 commit comments

Comments
 (0)