Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reserve __temporal prefix #410

Merged
merged 5 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ dotnet_diagnostic.CA2237.severity = none
# Warn on unused imports
dotnet_diagnostic.IDE0005.severity = warning

# Don't warn on using var
dotnet_diagnostic.IDE0008.severity = none
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you remember, am a bit curious where in code this was triggered

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All over. I think it's just a matter of how the default "low" severity shows up in different lang servers. All the low severity stuff shows up in omnisharp, which can be kinda nice to auto fix some stuff sometimes, but, this one is obviously a little silly.


# Cannot use range operator on older versions
dotnet_diagnostic.IDE0057.severity = none

Expand Down Expand Up @@ -186,4 +189,4 @@ dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggest
#Style - Pattern matching

#prefer pattern matching instead of is expression with type casts
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
3 changes: 3 additions & 0 deletions omnisharp.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"FormattingOptions": {
"EnableEditorConfigSupport": true
},
"RoslynExtensionsOptions": {
"enableAnalyzersSupport": true
}
}
8 changes: 7 additions & 1 deletion src/Temporalio/Activities/ActivityDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Temporalio.Runtime;

namespace Temporalio.Activities
{
Expand Down Expand Up @@ -293,6 +294,11 @@ private static ActivityDefinition Create(
Func<object?[], object?> invoker,
MethodInfo? methodInfo)
{
if (name != null && name.StartsWith(TemporalRuntime.ReservedNamePrefix))
{
throw new ArgumentException(
$"Activity name {name} cannot start with {TemporalRuntime.ReservedNamePrefix}");
}
// If there is a null name, which means dynamic, there must only be one parameter type
// and it must be varargs IRawValue
if (name == null && (
Expand Down Expand Up @@ -320,4 +326,4 @@ private static ActivityDefinition Create(
return new(name, returnType, parameterTypes, requiredParameterCount, invoker, methodInfo);
}
}
}
}
5 changes: 5 additions & 0 deletions src/Temporalio/Runtime/TemporalRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ namespace Temporalio.Runtime
/// </remarks>
public sealed class TemporalRuntime
{
/// <summary>
/// Prefix for reserved handler and definition names.
/// </summary>
internal const string ReservedNamePrefix = "__temporal";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be __temporal_ per temporalio/features#576

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See up here: #410 (comment)

Yeah, that's what's written there, but, also that just seems wrong. Probably we should change that spec.


private static readonly Lazy<TemporalRuntime> LazyDefault =
new(() => new TemporalRuntime(new TemporalRuntimeOptions()));

Expand Down
2 changes: 1 addition & 1 deletion src/Temporalio/Worker/ActivityWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -722,4 +722,4 @@ public override void Heartbeat(HeartbeatInput input)
}
}
}
}
}
2 changes: 1 addition & 1 deletion src/Temporalio/Worker/TemporalWorkerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -424,4 +424,4 @@ internal void OnTaskCompleted(WorkflowInstance instance, Exception? failureExcep
}
}
}
}
}
23 changes: 18 additions & 5 deletions src/Temporalio/Worker/WorkflowInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using Temporalio.Common;
using Temporalio.Converters;
using Temporalio.Exceptions;
using Temporalio.Runtime;
using Temporalio.Worker.Interceptors;
using Temporalio.Workflows;

Expand Down Expand Up @@ -939,7 +940,11 @@ private Task ApplyDoUpdateAsync(DoUpdate update)
var updates = mutableUpdates.IsValueCreated ? mutableUpdates.Value : Definition.Updates;
if (!updates.TryGetValue(update.Name, out var updateDefn))
{
updateDefn = DynamicUpdate;
// Do not fall back onto dynamic update if using the reserved prefix
if (!update.Name.StartsWith(TemporalRuntime.ReservedNamePrefix))
{
updateDefn = DynamicUpdate;
}
if (updateDefn == null)
{
var knownUpdates = updates.Keys.OrderBy(k => k);
Expand Down Expand Up @@ -1140,13 +1145,13 @@ private void ApplyQueryWorkflow(QueryWorkflow query)
if (query.QueryType == "__stack_trace")
{
Func<string> getter = GetStackTrace;
queryDefn = WorkflowQueryDefinition.CreateWithoutAttribute(
queryDefn = WorkflowQueryDefinition.CreateWithoutAttributeReservedName(
"__stack_trace", getter);
}
else if (query.QueryType == "__temporal_workflow_metadata")
{
Func<Api.Sdk.V1.WorkflowMetadata> getter = GetWorkflowMetadata;
queryDefn = WorkflowQueryDefinition.CreateWithoutAttribute(
queryDefn = WorkflowQueryDefinition.CreateWithoutAttributeReservedName(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these two, we need to not go through interceptors to invoke them. Would be ok if they aren't even "query definitions" and just invoked directly or something (also saves you from having that bypass constructor param for query definition)

"__temporal_workflow_metadata", getter);
}
else
Expand All @@ -1155,7 +1160,11 @@ private void ApplyQueryWorkflow(QueryWorkflow query)
var queries = mutableQueries.IsValueCreated ? mutableQueries.Value : Definition.Queries;
if (!queries.TryGetValue(query.QueryType, out queryDefn))
{
queryDefn = DynamicQuery;
// Do not fall back onto dynamic query if using the reserved prefix
if (!query.QueryType.StartsWith(TemporalRuntime.ReservedNamePrefix))
{
queryDefn = DynamicQuery;
}
if (queryDefn == null)
{
var knownQueries = queries.Keys.OrderBy(k => k);
Expand Down Expand Up @@ -1267,7 +1276,11 @@ private void ApplySignalWorkflow(SignalWorkflow signal)
var signals = mutableSignals.IsValueCreated ? mutableSignals.Value : Definition.Signals;
if (!signals.TryGetValue(signal.SignalName, out var signalDefn))
{
signalDefn = DynamicSignal;
// Do not fall back onto dynamic signal if using the reserved prefix
if (!signal.SignalName.StartsWith(TemporalRuntime.ReservedNamePrefix))
{
signalDefn = DynamicSignal;
}
if (signalDefn == null)
{
// No definition found, buffer
Expand Down
2 changes: 1 addition & 1 deletion src/Temporalio/Workflows/Workflow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1339,4 +1339,4 @@ public static class Unsafe
public static bool IsReplaying => Context.IsReplaying;
}
}
}
}
9 changes: 8 additions & 1 deletion src/Temporalio/Workflows/WorkflowDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Temporalio.Runtime;

namespace Temporalio.Workflows
{
Expand Down Expand Up @@ -427,6 +428,12 @@ public static WorkflowDefinition Create(
}
}

// Verify that registered names to not use our reserved prefix
if (name != null && name.StartsWith(TemporalRuntime.ReservedNamePrefix))
{
errs.Add($"Workflow name {name} cannot start with {TemporalRuntime.ReservedNamePrefix}");
}

// If there are any errors, throw
if (errs.Count > 0)
{
Expand Down Expand Up @@ -525,4 +532,4 @@ private static bool IsDefinedOnBase<T>(MethodInfo method)
}
}
}
}
}
40 changes: 38 additions & 2 deletions src/Temporalio/Workflows/WorkflowQueryDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Concurrent;
using System.Reflection;
using System.Threading.Tasks;
using Temporalio.Runtime;

namespace Temporalio.Workflows
{
Expand All @@ -10,11 +11,31 @@ namespace Temporalio.Workflows
/// </summary>
public class WorkflowQueryDefinition
{
/// <summary>
/// All known reserved query handler prefixes.
/// </summary>
internal static readonly string[] ReservedQueryHandlerPrefixes =
{
TemporalRuntime.ReservedNamePrefix,
"__stack_trace",
"__enhanced_stack_trace",
};

private static readonly ConcurrentDictionary<MethodInfo, WorkflowQueryDefinition> MethodDefinitions = new();
private static readonly ConcurrentDictionary<PropertyInfo, WorkflowQueryDefinition> PropertyDefinitions = new();

private WorkflowQueryDefinition(string? name, string? description, MethodInfo? method, Delegate? del)
private WorkflowQueryDefinition(string? name, string? description, MethodInfo? method, Delegate? del, bool bypassReserved = false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you now remove this constructor parameter for bypassing since it is never true anymore?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woops, thanks

{
if (!bypassReserved && name != null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to save lines, add using System.Linq; import and then:

Suggested change
if (!bypassReserved && name != null)
if (!bypassReserved && name != null && ReservedQueryHandlerPrefixes.Any(p => name.StartsWith(p))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't save all that much if you want to keep the prefix name in the error which I do, but, changed.

{
foreach (var reservedQ in ReservedQueryHandlerPrefixes)
{
if (name.StartsWith(reservedQ))
{
throw new ArgumentException($"Query handler name {name} cannot start with {reservedQ}");
}
}
}
Name = name;
Description = description;
Method = method;
Expand Down Expand Up @@ -106,6 +127,21 @@ public static WorkflowQueryDefinition CreateWithoutAttribute(
return new(name, description, null, del);
}

/// <summary>
/// Internal version of <see cref="CreateWithoutAttribute" /> that bypasses reserved name checks.
/// </summary>
/// <param name="name">Query name. Null for dynamic query.</param>
/// <param name="del">Query delegate.</param>
/// <param name="description">Optional description. WARNING: This setting is experimental.
/// </param>
/// <returns>Query definition.</returns>
internal static WorkflowQueryDefinition CreateWithoutAttributeReservedName(
string name, Delegate del, string? description = null)
{
AssertValid(del.Method, dynamic: name == null);
return new(name, description, null, del, bypassReserved: true);
}

/// <summary>
/// Gets the query name for calling or fail if no attribute or if dynamic.
/// </summary>
Expand Down Expand Up @@ -168,4 +204,4 @@ private static void AssertValid(MethodInfo method, bool dynamic)
}
}
}
}
}
7 changes: 6 additions & 1 deletion src/Temporalio/Workflows/WorkflowSignalDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Concurrent;
using System.Reflection;
using System.Threading.Tasks;
using Temporalio.Runtime;

namespace Temporalio.Workflows
{
Expand All @@ -19,6 +20,10 @@ private WorkflowSignalDefinition(
Delegate? del,
HandlerUnfinishedPolicy unfinishedPolicy)
{
if (name != null && name.StartsWith(TemporalRuntime.ReservedNamePrefix))
{
throw new ArgumentException($"Signal handler name {name} cannot start with {TemporalRuntime.ReservedNamePrefix}");
}
Name = name;
Description = description;
Method = method;
Expand Down Expand Up @@ -146,4 +151,4 @@ private static void AssertValid(MethodInfo method, bool dynamic)
}
}
}
}
}
7 changes: 6 additions & 1 deletion src/Temporalio/Workflows/WorkflowUpdateDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Temporalio.Runtime;

namespace Temporalio.Workflows
{
Expand All @@ -22,6 +23,10 @@ private WorkflowUpdateDefinition(
Delegate? validatorDel,
HandlerUnfinishedPolicy unfinishedPolicy)
{
if (name != null && name.StartsWith(TemporalRuntime.ReservedNamePrefix))
{
throw new ArgumentException($"Update handler name {name} cannot start with {TemporalRuntime.ReservedNamePrefix}");
}
Name = name;
Description = description;
Method = method;
Expand Down Expand Up @@ -190,4 +195,4 @@ private static void AssertValid(
}
}
}
}
}
12 changes: 11 additions & 1 deletion tests/Temporalio.Tests/Activities/ActivityDefinitionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,13 @@ public async Task Create_WithLambda_DoesNotHaveValidMethodInfo()
Assert.Null(defn.MethodInfo);
}

[Fact]
public void Create_With_Reserved_Name_Throws()
{
var exc = Assert.ThrowsAny<Exception>(() => ActivityDefinition.Create(IHaveABadName));
Assert.Contains("Activity name __temporal_badname cannot start with __temporal", exc.Message);
}

protected static void BadAct1()
{
}
Expand All @@ -220,6 +227,9 @@ protected static void BadAct2(ref string foo)
[Activity]
protected static Task GoodAct1Async() => Task.CompletedTask;

[Activity("__temporal_badname")]
protected static Task IHaveABadName() => Task.CompletedTask;

public static class GoodActivityClassStatic
{
[Activity]
Expand Down Expand Up @@ -252,4 +262,4 @@ public class GoodActivityClassInstance
[Activity]
public int MyActivity(int param) => param + 5;
}
}
}
11 changes: 11 additions & 0 deletions tests/Temporalio.Tests/Worker/WorkflowWorkerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2997,6 +2997,17 @@ await ExecuteWorkerAsync<DynamicWorkflow>(
await handle.SignalAsync(wf => wf.SomeSignalAsync("signal arg"));
Assert.Equal("done", await handle.QueryAsync(wf => wf.SomeQuery("query arg")));
Assert.Equal("done", await handle.ExecuteUpdateAsync(wf => wf.SomeUpdateAsync("update arg")));

// Verify that invocations using the reserved prefix are not forwarded to dynamic
// handlers
await handle.SignalAsync($"{TemporalRuntime.ReservedNamePrefix}_whatever", new[] { "signal arg 1" });
var queryExc2 = await Assert.ThrowsAsync<WorkflowQueryFailedException>(
() => handle.QueryAsync<string>($"{TemporalRuntime.ReservedNamePrefix}_whatever", new[] { "query arg 1" }));
Assert.Contains("not found", queryExc2.Message);
var updateExc = await Assert.ThrowsAsync<WorkflowUpdateFailedException>(
() => handle.ExecuteUpdateAsync<string>($"{TemporalRuntime.ReservedNamePrefix}_whatever", new[] { "update arg 1" }));
Assert.Contains("not found", updateExc.InnerException?.Message);

// Event list must be collected before the WF finishes, since when it finishes it
// will be evicted from the cache and the first SomeQuery event will not exist upon
// replay.
Expand Down
Loading
Loading