Skip to content

Commit

Permalink
Add CallTarget support for ValueTask in .NET FX and < .NET Core 3.1 (
Browse files Browse the repository at this point in the history
…#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
  • Loading branch information
andrewlock authored Jan 9, 2025
1 parent 3716bd0 commit 2fbf8d0
Show file tree
Hide file tree
Showing 16 changed files with 905 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// <copyright file="IValueTaskDuckType.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

#if !NETCOREAPP3_1_OR_GREATER
#nullable enable
using System.Threading.Tasks;

namespace Datadog.Trace.ClrProfiler.CallTarget.Handlers.Continuations;

internal interface IValueTaskDuckType
{
bool IsCompletedSuccessfully { get; }

Task AsTask();
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// <copyright file="ValueTaskActivator.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

#if !NETCOREAPP3_1_OR_GREATER
#nullable enable

using System;
using System.Reflection.Emit;
using System.Threading.Tasks;
using Datadog.Trace.DuckTyping;
using Datadog.Trace.Logging;
using Datadog.Trace.Util;

namespace Datadog.Trace.ClrProfiler.CallTarget.Handlers.Continuations;

internal static class ValueTaskActivator<TValueTask>
{
private static readonly Func<Task, TValueTask> Activator;

static ValueTaskActivator()
{
try
{
Activator = CreateActivator();
}
catch (Exception ex)
{
DatadogLogging.GetLoggerFor<ActivatorHelper>()
.Error(ex, "Error creating the custom activator for: {Type}", typeof(TValueTask).FullName);

// Unfortunately this will box the ValueTask, but I think it's still the best we can do in this scenario
Activator = FallbackActivator;
}
}

// Internal for testing
internal static Func<Task, TValueTask> CreateActivator()
{
var valueTaskType = typeof(TValueTask);
var ctor = valueTaskType.GetConstructor([typeof(Task)])!;

var createValueTaskMethod = new DynamicMethod(
$"TypeActivator" + valueTaskType.Name,
returnType: valueTaskType,
parameterTypes: [typeof(Task)],
typeof(DuckType).Module,
true);

var il = createValueTaskMethod.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Newobj, ctor);
il.Emit(OpCodes.Ret);

return (Func<Task, TValueTask>)createValueTaskMethod.CreateDelegate(typeof(Func<Task, TValueTask>));
}

// Internal for testing
internal static TValueTask FallbackActivator(Task task)
=> (TValueTask)System.Activator.CreateInstance(typeof(TValueTask), task)!;

public static TValueTask CreateInstance(Task task) => Activator(task);
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// <copyright file="ValueTaskActivator`1.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

#if !NETCOREAPP3_1_OR_GREATER
#nullable enable

using System;
using System.Reflection.Emit;
using System.Threading.Tasks;
using Datadog.Trace.DuckTyping;
using Datadog.Trace.Logging;
using Datadog.Trace.Util;

#pragma warning disable SA1649 // File name must match first type name

namespace Datadog.Trace.ClrProfiler.CallTarget.Handlers.Continuations;

internal static class ValueTaskActivator<TValueTask, TResult>
{
private static readonly Func<Task<TResult>, TValueTask> TaskActivator;
private static readonly Func<TResult, TValueTask> ResultActivator;

static ValueTaskActivator()
{
try
{
TaskActivator = CreateTaskActivator();
ResultActivator = CreateResultActivator();
}
catch (Exception ex)
{
DatadogLogging.GetLoggerFor<ActivatorHelper>()
.Error(ex, "Error creating the custom activator for: {Type}", typeof(TValueTask).FullName);

// Unfortunately this will box the ValueTask, but I think it's still the best we can do in this scenario
TaskActivator = FallbackTaskActivator;
ResultActivator = FallbackResultActivator;
}
}

// Internal for testing
internal static Func<Task<TResult>, TValueTask> CreateTaskActivator()
{
var valueTaskType = typeof(TValueTask);
var ctor = valueTaskType.GetConstructor([typeof(Task<TResult>)])!;

var createValueTaskMethod = new DynamicMethod(
$"TypeActivatorTask" + valueTaskType.Name,
returnType: valueTaskType,
parameterTypes: [typeof(Task<TResult>)],
typeof(DuckType).Module,
true);

var il = createValueTaskMethod.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Newobj, ctor);
il.Emit(OpCodes.Ret);

return (Func<Task<TResult>, TValueTask>)createValueTaskMethod.CreateDelegate(typeof(Func<Task<TResult>, TValueTask>));
}

// Internal for testing
internal static Func<TResult, TValueTask> CreateResultActivator()
{
var valueTaskType = typeof(TValueTask);
var ctor = valueTaskType.GetConstructor([typeof(TResult)])!;

var createValueTaskMethod = new DynamicMethod(
$"TypeActivatorResult" + valueTaskType.Name,
returnType: valueTaskType,
parameterTypes: [typeof(TResult)],
typeof(DuckType).Module,
true);

var il = createValueTaskMethod.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Newobj, ctor);
il.Emit(OpCodes.Ret);

return (Func<TResult, TValueTask>)createValueTaskMethod.CreateDelegate(typeof(Func<TResult, TValueTask>));
}

// Internal for testing
internal static TValueTask FallbackTaskActivator(Task<TResult> task)
=> (TValueTask)Activator.CreateInstance(typeof(TValueTask), task)!;

internal static TValueTask FallbackResultActivator(TResult task)
=> (TValueTask)Activator.CreateInstance(typeof(TValueTask), task)!;

public static TValueTask CreateInstance(Task<TResult> task) => TaskActivator(task);

public static TValueTask CreateInstance(TResult result) => ResultActivator(result);
}
#endif
Loading

0 comments on commit 2fbf8d0

Please sign in to comment.