【问题标题】:AsyncLocal test failing when run alongside other tests与其他测试一起运行时 AsyncLocal 测试失败
【发布时间】:2022-02-05 06:35:39
【问题描述】:

我正在尝试创建一个名为 AuditScope 的自定义范围类,从而可以通过 AuditScope.Current 访问当前范围。

如果有嵌套作用域,则当前作用域是嵌套最多的作用域。

我希望这是线程安全的,所以我使用AsyncLocal 来确保当前范围属于当前异步上下文,并且不会与其他请求发生冲突。这类似于TransactionScope 类,如果你们中有人遇到过的话。

这是我的作用域类:

public sealed class AuditScope : IDisposable
{
    private static readonly AsyncLocal<Stack<AuditScope>> ScopeStack = new();

    public int ExecutedByUserId { get; }

    public AuditScope(int executedByUserId)
    {
        ExecutedByUserId = executedByUserId;

        if (ScopeStack.Value == null)
        {
            ScopeStack.Value = new Stack<AuditScope>();
        }

        ScopeStack.Value.Push(this);
    }

    public static AuditScope? Current
    {
        get
        {
            if (ScopeStack.Value == null || ScopeStack.Value.Count == 0)
            {
                return null;
            }

            return ScopeStack.Value.Peek();
        }
    }

    public void Dispose()
    {
        ScopeStack.Value?.Pop();
    }
}

我所有的测试都单独通过,但是如果我同时运行它们,一个测试始终失败:

[Test]
public async Task GivenThreadCreatesScope_AndSecondThreadCreatesScope_WhenCurrentScopeAccessedOnBothThreads_ThenCorrectScopeReturned()
{
    // Arrange
    static async Task createScopeWithLifespan(int lifespanInMilliseconds)
    {
        // This line throws the error, saying it is not null (for the 2000ms scope)
        // No scope has been created yet for this async context, so current should be null
        Assert.IsNull(AuditScope.Current);

        using (var scope = new AuditScope(1))
        {
            // Scope has been created, so current should match
            Assert.AreEqual(scope, AuditScope.Current);

            await Task.Delay(lifespanInMilliseconds);

            // Scope has not been disposed, so current should still match
            Assert.AreEqual(scope, AuditScope.Current);
        }

        // Scope has been disposed, so current should be null
        Assert.IsNull(AuditScope.Current);
    }

    // Act & Assert
    await Task.WhenAll(
        createScopeWithLifespan(1000),
        createScopeWithLifespan(2000));
}

当然,由于using 语句在不同的上下文中,这应该有效吗?为什么单独运行时通过,而与其他测试一起运行时却不通过?

为了完整起见,请参阅下面我正在运行的其他测试,但我严重怀疑它们是否会直接影响它们:

[Test]
public void GivenNoCurrentScope_WhenCurrentScopeAccessed_ThenNull()
{
    // Act
    var result = AuditScope.Current;

    // Arrange
    Assert.Null(result);
}

[Test]
public void GivenScope_WhenScopeDisposed_ThenNull()
{
    // Arrange
    using (var scope = new AuditScope(1))
    {
    }

    // Act
    var result = AuditScope.Current;

    // Arrange
    Assert.Null(result);
}

[Test]
public void GivenScopeCreated_WhenCurrentScopeAccessed_ThenScopeReturned()
{
    // Arrange
    using (var scope = new AuditScope(1))
    {
        // Act
        var result = AuditScope.Current;

        // Arrange
        Assert.NotNull(result);
        Assert.AreEqual(scope, result);
    }
}

[Test]
public void GivenNestedScopeCreated_WhenCurrentScopeAccessed_ThenNestedScopeReturned()
{
    // Arrange
    using (var scope = new AuditScope(1))
    {
        using (var nestedScope = new AuditScope(2))
        {
            // Act
            var result = AuditScope.Current;

            // Arrange
            Assert.NotNull(result);
            Assert.AreEqual(nestedScope, result);
        }
    }
}

[Test]
public void GivenNestedScopeCreated_WhenNestedScopeDisposed_ThenCurrentScopeRevertsToParent()
{
    // Arrange
    using (var scope = new AuditScope(1))
    {
        using (var nestedScope = new AuditScope(2))
        {
        }

        // Act
        var result = AuditScope.Current;

        // Arrange
        Assert.NotNull(result);
        Assert.AreEqual(scope, result);
    }
}

【问题讨论】:

    标签: c# .net asynchronous .net-core scope


    【解决方案1】:

    原来一定是某个地方存在参考问题,因为将Stack&lt;AuditScope&gt; 替换为ImmutableStack&lt;AuditScope&gt; 解决了这个问题。

    【讨论】:

      【解决方案2】:

      我遇到了同样的问题。和我一样,我相信您的问题在于您的 Dispose:

      public void Dispose()
      {
          ScopeStack.Value?.Pop();
      }
      

      TLDR;是不是你改成这个我都愿意打赌……

      public void Dispose()
      {
          ScopeStack.Value?.Pop();
          if(ScopeStack.Value.Count == 0)
               ScopeStack.Value = null;
      }
      

      ...即使没有 ImmutableStack,您的代码也会按预期工作。

      AsyncLocal 感觉类似于 ThreadStatic,并且在异步上下文(如 MVC)中使用时。它提供了与每个调用上下文相同的分离,但解决了由不同线程服务的上下文所产生的问题。

      但与 ThreadStatic 不同,AsyncLocal 上下文确实向下流向同一调用上下文中的子线程。

      在我们的单元测试中,初始调用上下文是测试运行器,它可能在同一个线程中运行所有其他测试。当它进入有问题的测试时,您的 ScopeStack 已初始化,然后传递到子线程。您可以通过在执行任务之前简单地创建和销毁 ScopeStack 来重现相同的错误。

      将其设置为 null 允许它在线程内部重新初始化,并且 AsyncLocal 不会回流(子线程将全部初始化它们自己的,因为他们不会看到它已经建立)。

      ImmutableStack 修复了它,因为即使它向下流动,您也永远不会真正对原始文件进行更改。这里唯一的问题是,如果您想进行一些无效的嵌套检测或不允许在子级之前从父级进行处置,您将无法看到子级范围所做的此类更改。当然,如果您需要从同一个父级启动多个兄弟姐妹(此时它们不能共享堆栈),这可能已经遥不可及。

      【讨论】:

        猜你喜欢
        • 2015-03-20
        • 1970-01-01
        • 1970-01-01
        • 2022-10-13
        • 1970-01-01
        • 2015-05-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多