Is LogicalOperationStack incompatible with async in .Net 4.5

前端 未结 3 1411
萌比男神i
萌比男神i 2021-02-04 04:16

Trace.CorrelationManager.LogicalOperationStack enables having nested logical operation identifiers where the most common case is logging (NDC). Should it still work

3条回答
  •  没有蜡笔的小新
    2021-02-04 04:34

    If you're still interested in this, I believe it's a bug in how they flow LogicalOperationStack and I think it's a good idea to report it.

    They give special treatment to LogicalOperationStack's stack here in LogicalCallContext.Clone, by doing a deep copy (unlike with other data stored via CallContext.LogicalSetData/LogicalGetData, on which only a shallow copy is performed).

    This LogicalCallContext.Clone is called every time ExecutionContext.CreateCopy or ExecutionContext.CreateMutableCopy is called to flow the ExecutionContext.

    Based on your code, I did a little experiment by providing my own mutable stack for "System.Diagnostics.Trace.CorrelationManagerSlot" slot in LogicalCallContext, to see when and how many times it actually gets cloned.

    The code:

    using System;
    using System.Collections;
    using System.Diagnostics;
    using System.Linq;
    using System.Runtime.Remoting.Messaging;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace ConsoleApplication
    {
        class Program
        {
            static readonly string CorrelationManagerSlot = "System.Diagnostics.Trace.CorrelationManagerSlot";
    
            public static void ShowCorrelationManagerStack(object where)
            {
                object top = "null";
                var stack = (MyStack)CallContext.LogicalGetData(CorrelationManagerSlot);
                if (stack.Count > 0)
                    top = stack.Peek();
    
                Console.WriteLine("{0}: MyStack Id={1}, Count={2}, on thread {3}, top: {4}",
                    where, stack.Id, stack.Count, Environment.CurrentManagedThreadId, top);
            }
    
            private static void Main()
            {
                CallContext.LogicalSetData(CorrelationManagerSlot, new MyStack());
    
                OuterOperationAsync().Wait();
                Console.ReadLine();
            }
    
            private static async Task OuterOperationAsync()
            {
                ShowCorrelationManagerStack(1.1);
    
                using (LogicalFlow.StartScope())
                {
                    ShowCorrelationManagerStack(1.2);
                    Console.WriteLine("\t" + LogicalFlow.CurrentOperationId);
                    await InnerOperationAsync();
                    ShowCorrelationManagerStack(1.3);
                    Console.WriteLine("\t" + LogicalFlow.CurrentOperationId);
                    await InnerOperationAsync();
                    ShowCorrelationManagerStack(1.4);
                    Console.WriteLine("\t" + LogicalFlow.CurrentOperationId);
                }
    
                ShowCorrelationManagerStack(1.5);
            }
    
            private static async Task InnerOperationAsync()
            {
                ShowCorrelationManagerStack(2.1);
                using (LogicalFlow.StartScope())
                {
                    ShowCorrelationManagerStack(2.2);
                    await Task.Delay(100);
                    ShowCorrelationManagerStack(2.3);
                }
                ShowCorrelationManagerStack(2.4);
            }
        }
    
        public class MyStack : Stack, ICloneable
        {
            public static int s_Id = 0;
    
            public int Id { get; private set; }
    
            object ICloneable.Clone()
            {
                var cloneId = Interlocked.Increment(ref s_Id); ;
                Console.WriteLine("Cloning MyStack Id={0} into {1} on thread {2}", this.Id, cloneId, Environment.CurrentManagedThreadId);
    
                var clone = new MyStack();
                clone.Id = cloneId;
    
                foreach (var item in this.ToArray().Reverse())
                    clone.Push(item);
    
                return clone;
            }
        }
    
        public static class LogicalFlow
        {
            public static Guid CurrentOperationId
            {
                get
                {
                    return Trace.CorrelationManager.LogicalOperationStack.Count > 0
                        ? (Guid)Trace.CorrelationManager.LogicalOperationStack.Peek()
                        : Guid.Empty;
                }
            }
    
            public static IDisposable StartScope()
            {
                Program.ShowCorrelationManagerStack("Before StartLogicalOperation");
                Trace.CorrelationManager.StartLogicalOperation();
                Program.ShowCorrelationManagerStack("After StartLogicalOperation");
                return new Stopper();
            }
    
            private static void StopScope()
            {
                Program.ShowCorrelationManagerStack("Before StopLogicalOperation");
                Trace.CorrelationManager.StopLogicalOperation();
                Program.ShowCorrelationManagerStack("After StopLogicalOperation");
            }
    
            private class Stopper : IDisposable
            {
                private bool _isDisposed;
                public void Dispose()
                {
                    if (!_isDisposed)
                    {
                        StopScope();
                        _isDisposed = true;
                    }
                }
            }
        }
    }
    

    The result is quite surprising. Even though there're only two threads involved in this async workflow, the stack gets cloned as many as 4 times. And the problem is, the matching Stack.Push and Stack.Pop operations (called by StartLogicalOperation/StopLogicalOperation) operate on the different, non-matching clones of the stack, thus disbalancing the "logical" stack. That's where the bug lays in.

    This indeed makes LogicalOperationStack totally unusable across async calls, even though there's no concurrent forks of tasks.

    Updated, I also did a little research about how it may behave for synchronous calls, to address these comments:

    Agreed, not a dupe. Did you check if it works as expected on the same thread, e.g. if you replace await Task.Delay(100) with Task.Delay(100).Wait()? – Noseratio Feb 27 at 21:00

    @Noseratio yes. It works of course, because there's only a single thread (and so a single CallContext). It's as if the method wasn't async to begin with. – i3arnon Feb 27 at 21:01

    Single thread doesn't mean single CallContext. Even for synchronous continuations on the same single thread the execution context (and its inner LogicalCallContext) can get cloned. Example, using the above code:

    private static void Main()
    {
        CallContext.LogicalSetData(CorrelationManagerSlot, new MyStack());
    
        ShowCorrelationManagerStack(0.1);
    
        CallContext.LogicalSetData("slot1", "value1");
        Console.WriteLine(CallContext.LogicalGetData("slot1"));
    
        Task.FromResult(0).ContinueWith(t =>
            {
                ShowCorrelationManagerStack(0.2);
    
                CallContext.LogicalSetData("slot1", "value2");
                Console.WriteLine(CallContext.LogicalGetData("slot1"));
            }, 
            CancellationToken.None,
            TaskContinuationOptions.ExecuteSynchronously,
            TaskScheduler.Default);
    
        ShowCorrelationManagerStack(0.3);
        Console.WriteLine(CallContext.LogicalGetData("slot1"));
    
        // ...
    }
    

    Output (note how we lose "value2"):

    0.1: MyStack Id=0, Count=0, on thread 9, top:
    value1
    Cloning MyStack Id=0 into 1 on thread 9
    0.2: MyStack Id=1, Count=0, on thread 9, top:
    value2
    0.3: MyStack Id=0, Count=0, on thread 9, top:
    value1
    

提交回复
热议问题