Skip to content

Deadlock when synchronously awaiting an async method in GitHub Actions #2587

Closed
@YegorStepanov

Description

@YegorStepanov

Deadlock in Windows/Linux when 2 tests wait the tasks in sync.
Deadlock in Windows/Linux/macOS when 3 tests wait the tasks in sync. (macOS has 3 threads in GH Actions)

I cannot reproduce it locally, but GH Action always falls.

Other test frameworks run fine.

For 2 tests:
image
https://github.com/YegorStepanov/xunit-deadlock/actions/runs/3153937997

For 3 tests:
image
https://github.com/YegorStepanov/xunit-deadlock/actions/runs/3153957706

(I have set a 10 min timeout in GH Actions, without a timeout they fall after 6 hours)

Code

[Fact]
public void Method1()
{
    Task<int> task = new BenchmarkClass1().Method();
    bool isAsyncMethod = TaskHelper.TryAwaitTask(task, out object result);

    Assert.True(isAsyncMethod);
    Assert.Equal(1, result);
}

[Fact]
public void Method2()
{
    Task<int> task = new BenchmarkClass2().Method();
    bool isAsyncMethod = TaskHelper.TryAwaitTask(task, out object result);

    Assert.True(isAsyncMethod);
    Assert.Equal(42, result);
}


public class BenchmarkClass1
{
    public async Task<int> Method()
    {
        await Task.Delay(1);
        return 1;
    }
}

public class BenchmarkClass2
{
    public async Task<int> Method()
    {
        await Task.Delay(1);
        return 42;
    }
}

public static class TaskHelper
{
    // we need await task in sync and return result.
    // returns true if task
    public static bool TryAwaitTask(object task, out object result)
    {
        result = null;

        if (task is null)
        {
            return false;
        }

        // ValueTask<T>
        var returnType = task.GetType();
        if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(ValueTask<>))
        {
            var asTaskMethod = task.GetType().GetMethod("AsTask");
            task = asTaskMethod.Invoke(task, null);
        }

        if (task is ValueTask valueTask)
            task = valueTask.AsTask();

        // Task or Task<T>
        if (task is Task t)
        {
            if (TryGetTaskResult(t, out var taskResult))
                result = taskResult;

            return true;
        }

        return false;
    }

    // https://stackoverflow.com/a/52500763
    private static bool TryGetTaskResult(Task task, out object result)
    {
        result = null;

        var voidTaskType = typeof(Task<>).MakeGenericType(Type.GetType("System.Threading.Tasks.VoidTaskResult"));
        if (voidTaskType.IsInstanceOfType(task))
        {
            task.GetAwaiter().GetResult();
            return false;
        }

        var property = task.GetType().GetProperty("Result", BindingFlags.Public | BindingFlags.Instance);
        if (property is null)
        {
            return false;
        }

        result = property.GetValue(task);
        return true;
    }
}

Repro: https://github.com/YegorStepanov/xunit-deadlock
Occurred in: dotnet/BenchmarkDotNet#2114. Check comments/CI builds in PR, they can help.

If I add [Collection("Dissable parallelisation")] or move the tests to one class, there is no deadlock.

cc @timcassell

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions