Skip to content

Commit 2fbf8d0

Browse files
authored
Add CallTarget support for ValueTask in .NET FX and < .NET Core 3.1 (#6480)
## Summary of changes Add support for correct `CallTarget` instrumentation of methods that return `ValueTask` in < .NET Core 3.1 or .NET Framework ## Reason for change We already support instrumenting methods that returns `ValueTask` in .NET Core 3.1. However, in .NET Framework or .NET Standard 2.0, this support is [provided by a package](https://www.nuget.org/packages/System.Threading.Tasks.Extensions), and we currently _don't_ support instrumenting these methods. Or rather, we just ignore the `OnAsyncMethodEnd` in integrations in these cases. ## Implementation details We already support `ValueTask` in more recent frameworks, and the support is very similar to our `Task` support. Unfortunately, in .NET FX we can't reference the `ValueTask` type itself. To work around this, we do the following: - Detect that the return type is `ValueTask` or `ValueTask<T>` either by loading the type directly (.NET Core) or checking the type name (.NET Framework) - Duck-type the `ValueTask` to read the `IsCompletedSuccessfully` value. - If this is true, the task is already completed synchronously, and we can simply call the target method. - For `ValueTask<T>` we duck type `Result` and read that directly too. - If it _hasn't_ completed, we need to extract the `Task` from it. - Duck typing is used again to extract the `Task()` for uncompleted `ValueTask` - At this point, it's mostly a copy-paste of the existing `Task` integrations - Once the integration returns, we need to create a "new" `ValueTask` instead from the previous one - The semantics of `ValueTask` _require_ that we create a "fresh" one, we can't just "reuse" the one we got originally, because we've already retrieved the result/ awaited the inner task - Have to use `Activator`/`DynamicMethod` for this > [!WARNING] > The existing `ValueTask`/`ValueTask<>` and `Task`/`Task<>` integrations are written quite differently, and I'm not entirely sure why 🤔 Given the `ContinuationAction()` methods for both these cases operate on `Task`, I based on the new `ValueTask` integrations on the `Task` integrations, but if anyone has reasons why it shouldn't be, I'm all ears! ## Test coverage - Added `Task` and `ValueTask` tests to the `CallTargetNativeTests` integration tests. Previously we were only testing a single `Task` example, and that was somewhat insufficient - Update the `CallTargetNativeTests` to explicitly assert that the `OnAsyncMethodEnd` methods are called for `Task` / `ValueTask` method integrations. As we provide _both_ methods in our target integration, we were silently calling the wrong one for .NET FX - Prior to the fix in this PR, these updated tests would fail on < .NET Core 3.0 and .NET FX - Run the `ValueTaskAsyncContinutationGenerator` unit tests on all TFMs, not just .NET Core 3.1 - Unit tests for the `ValueTaskHelper` for checking if a type is a `ValueTask` - Unit tests for the `ValueTaskActivator` for creating a `ValueTask` from a `Task` or `Task<T>` - Verified it fixes the issues I was seeing in the RabbitMQ integration - #6479 ## Other details Required for - #6479
1 parent 3716bd0 commit 2fbf8d0

16 files changed

+905
-19
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// <copyright file="IValueTaskDuckType.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
#if !NETCOREAPP3_1_OR_GREATER
7+
#nullable enable
8+
using System.Threading.Tasks;
9+
10+
namespace Datadog.Trace.ClrProfiler.CallTarget.Handlers.Continuations;
11+
12+
internal interface IValueTaskDuckType
13+
{
14+
bool IsCompletedSuccessfully { get; }
15+
16+
Task AsTask();
17+
}
18+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// <copyright file="ValueTaskActivator.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
#if !NETCOREAPP3_1_OR_GREATER
7+
#nullable enable
8+
9+
using System;
10+
using System.Reflection.Emit;
11+
using System.Threading.Tasks;
12+
using Datadog.Trace.DuckTyping;
13+
using Datadog.Trace.Logging;
14+
using Datadog.Trace.Util;
15+
16+
namespace Datadog.Trace.ClrProfiler.CallTarget.Handlers.Continuations;
17+
18+
internal static class ValueTaskActivator<TValueTask>
19+
{
20+
private static readonly Func<Task, TValueTask> Activator;
21+
22+
static ValueTaskActivator()
23+
{
24+
try
25+
{
26+
Activator = CreateActivator();
27+
}
28+
catch (Exception ex)
29+
{
30+
DatadogLogging.GetLoggerFor<ActivatorHelper>()
31+
.Error(ex, "Error creating the custom activator for: {Type}", typeof(TValueTask).FullName);
32+
33+
// Unfortunately this will box the ValueTask, but I think it's still the best we can do in this scenario
34+
Activator = FallbackActivator;
35+
}
36+
}
37+
38+
// Internal for testing
39+
internal static Func<Task, TValueTask> CreateActivator()
40+
{
41+
var valueTaskType = typeof(TValueTask);
42+
var ctor = valueTaskType.GetConstructor([typeof(Task)])!;
43+
44+
var createValueTaskMethod = new DynamicMethod(
45+
$"TypeActivator" + valueTaskType.Name,
46+
returnType: valueTaskType,
47+
parameterTypes: [typeof(Task)],
48+
typeof(DuckType).Module,
49+
true);
50+
51+
var il = createValueTaskMethod.GetILGenerator();
52+
il.Emit(OpCodes.Ldarg_0);
53+
il.Emit(OpCodes.Newobj, ctor);
54+
il.Emit(OpCodes.Ret);
55+
56+
return (Func<Task, TValueTask>)createValueTaskMethod.CreateDelegate(typeof(Func<Task, TValueTask>));
57+
}
58+
59+
// Internal for testing
60+
internal static TValueTask FallbackActivator(Task task)
61+
=> (TValueTask)System.Activator.CreateInstance(typeof(TValueTask), task)!;
62+
63+
public static TValueTask CreateInstance(Task task) => Activator(task);
64+
}
65+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// <copyright file="ValueTaskActivator`1.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
#if !NETCOREAPP3_1_OR_GREATER
7+
#nullable enable
8+
9+
using System;
10+
using System.Reflection.Emit;
11+
using System.Threading.Tasks;
12+
using Datadog.Trace.DuckTyping;
13+
using Datadog.Trace.Logging;
14+
using Datadog.Trace.Util;
15+
16+
#pragma warning disable SA1649 // File name must match first type name
17+
18+
namespace Datadog.Trace.ClrProfiler.CallTarget.Handlers.Continuations;
19+
20+
internal static class ValueTaskActivator<TValueTask, TResult>
21+
{
22+
private static readonly Func<Task<TResult>, TValueTask> TaskActivator;
23+
private static readonly Func<TResult, TValueTask> ResultActivator;
24+
25+
static ValueTaskActivator()
26+
{
27+
try
28+
{
29+
TaskActivator = CreateTaskActivator();
30+
ResultActivator = CreateResultActivator();
31+
}
32+
catch (Exception ex)
33+
{
34+
DatadogLogging.GetLoggerFor<ActivatorHelper>()
35+
.Error(ex, "Error creating the custom activator for: {Type}", typeof(TValueTask).FullName);
36+
37+
// Unfortunately this will box the ValueTask, but I think it's still the best we can do in this scenario
38+
TaskActivator = FallbackTaskActivator;
39+
ResultActivator = FallbackResultActivator;
40+
}
41+
}
42+
43+
// Internal for testing
44+
internal static Func<Task<TResult>, TValueTask> CreateTaskActivator()
45+
{
46+
var valueTaskType = typeof(TValueTask);
47+
var ctor = valueTaskType.GetConstructor([typeof(Task<TResult>)])!;
48+
49+
var createValueTaskMethod = new DynamicMethod(
50+
$"TypeActivatorTask" + valueTaskType.Name,
51+
returnType: valueTaskType,
52+
parameterTypes: [typeof(Task<TResult>)],
53+
typeof(DuckType).Module,
54+
true);
55+
56+
var il = createValueTaskMethod.GetILGenerator();
57+
il.Emit(OpCodes.Ldarg_0);
58+
il.Emit(OpCodes.Newobj, ctor);
59+
il.Emit(OpCodes.Ret);
60+
61+
return (Func<Task<TResult>, TValueTask>)createValueTaskMethod.CreateDelegate(typeof(Func<Task<TResult>, TValueTask>));
62+
}
63+
64+
// Internal for testing
65+
internal static Func<TResult, TValueTask> CreateResultActivator()
66+
{
67+
var valueTaskType = typeof(TValueTask);
68+
var ctor = valueTaskType.GetConstructor([typeof(TResult)])!;
69+
70+
var createValueTaskMethod = new DynamicMethod(
71+
$"TypeActivatorResult" + valueTaskType.Name,
72+
returnType: valueTaskType,
73+
parameterTypes: [typeof(TResult)],
74+
typeof(DuckType).Module,
75+
true);
76+
77+
var il = createValueTaskMethod.GetILGenerator();
78+
il.Emit(OpCodes.Ldarg_0);
79+
il.Emit(OpCodes.Newobj, ctor);
80+
il.Emit(OpCodes.Ret);
81+
82+
return (Func<TResult, TValueTask>)createValueTaskMethod.CreateDelegate(typeof(Func<TResult, TValueTask>));
83+
}
84+
85+
// Internal for testing
86+
internal static TValueTask FallbackTaskActivator(Task<TResult> task)
87+
=> (TValueTask)Activator.CreateInstance(typeof(TValueTask), task)!;
88+
89+
internal static TValueTask FallbackResultActivator(TResult task)
90+
=> (TValueTask)Activator.CreateInstance(typeof(TValueTask), task)!;
91+
92+
public static TValueTask CreateInstance(Task<TResult> task) => TaskActivator(task);
93+
94+
public static TValueTask CreateInstance(TResult result) => ResultActivator(result);
95+
}
96+
#endif

0 commit comments

Comments
 (0)