WCF,异步和上下文的混乱

时间:2014-07-10 20:20:55

标签: wcf asynchronous ninject callstack ierrorhandler

好吧,我 将命名此和上下文问题,但显然标题中不允许使用 question 这个词。

无论如何,问题在于:我在我的WCF服务中使用IErrorHandler以便提供日志记录而不会混淆我的所有服务代码。到目前为止,这一点非常有效。但是,现在我正在尝试转向完全异步服务,我遇到了call stack being a return stack instead of a causality chain的问题。

现在,我尝试使用Stephen Cleary's logical call context MyStack,并结合Ninject的{​​{1}}扩展名。

Ninject:

Intercept

SimpleContextGenerator:

Bind<IThing>().To<Thing>()
    .Intercept()
    .With<SimpleContextGenerator>();

然而,问题有两个:1)public class SimpleContextGenerator : IInterceptor { public void Intercept(IInvocation invocation) { using (MyStack.Push( string.Join(".", invocation.Request.Method.DeclaringType.FullName, invocation.Request.Method.Name))) { invocation.Proceed(); } } } 在错误实际抛出之前完成,2)1甚至不重要因为整个上下文在我到达{{{{ 1}}。我可以在using中注明IErrorHandler中的代码,Pop中的MyStack CurrentContext.IsEmpty时注释true

所以,我的问题也是两部分:

1)有没有办法将上下文保持到ProvideFault来电?

2)如果没有,是否有其他方法可以在全球范围内记录 有权访问上下文的错误?

我使用的是.NET 4.5,Ninject 3.2和DynamicProxy 3.2。

说实话,我很高兴只知道抛出异常的地方 - 目前的课程和方法足以达到我的目的;不需要完整堆栈。

编辑:如果我使用IErrorHandler将其放入IErrorHandler,我可以保留它,直到我到达OperationContext。但是,我仍然不知道方法何时结束,所以我无法确定异常发生的位置。

1 个答案:

答案 0 :(得分:1)

为了以IErrorHandler中提供的方式跟踪堆栈,请使用IExtension<>

public class ContextStack : IExtension<OperationContext>
{

    // http://stackoverflow.com/a/1895958/128217

    private readonly LinkedList<Frame> _stack;

    private ContextStack()
    {
        _stack = new LinkedList<Frame>();
    }

    public LinkedList<Frame> Stack
    {
        get { return _stack; }
    }

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private static readonly object _locker = new object();
    public static ContextStack Current
    {
        get
        {
            ContextStack context = OperationContext.Current.Extensions.Find<ContextStack>();
            if (context == null)
            {
                lock (_locker)
                {
                    context = OperationContext.Current.Extensions.Find<ContextStack>();
                    if (context == null)
                    {
                        context = new ContextStack();
                        OperationContext.Current.Extensions.Add(context);
                    }
                }
            }
            return context;
        }
    }

    public IDisposable Push(Frame frame)
    {
        Stack.AddFirst(frame);
        return new PopWhenDisposed(frame, Stack);
    }

    public void Attach(OperationContext owner) { }
    public void Detach(OperationContext owner) { }

    private sealed class PopWhenDisposed : IDisposable
    {

        private bool _disposed;
        private readonly Frame _frame;
        private readonly LinkedList<Frame> _stack;

        public PopWhenDisposed(Frame frame, LinkedList<Frame> stack)
        {
            _frame = frame;
            _stack = stack;
        }

        public void Dispose()
        {
            if (_disposed)
            {
                return;
            }
            _stack.Remove(_frame);
            _disposed = true;
        }

    }

}

这里是正在跟踪的Frame

public class Frame
{

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly string _type;
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly string _method;
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly Parameter[] _parameters;

    public string Type { get { return _type; } }
    public string Method { get { return _method; } }
    public Parameter[] Parameters { get { return _parameters; } }

    public Task Task { get; private set; }
    public Exception Exception { get; private set; }

    public Frame(Type type, string method, params Parameter[] parameters)
    {
        _type = type.FullName;
        _method = method;
        _parameters = parameters;
    }

    public void SetTask(Task task)
    {
        if (Task != null)
        {
            throw new InvalidOperationException("Task is already set.");
        }
        Task = task;
    }

    public void SetException(Exception exception)
    {
        if (Exception != null)
        {
            throw new InvalidOperationException("Exception is already set.");
        }

        // Unwrap AggregateExceptions with a single inner exception.
        if (exception is AggregateException && ((AggregateException)exception).InnerExceptions.Count == 1)
        {
            Exception = exception.InnerException;
        }
        else
        {
            Exception = exception;
        }
    }

    public override string ToString()
    {
        StringBuilder sb = new StringBuilder(Type);
        sb.Append(".");
        sb.Append(Method);
        sb.Append("(");
        sb.Append(string.Join(", ", (object[])Parameters)); // Needed to pick an overload.
        sb.Append(")");
        return sb.ToString();
    }

}

Parameter

public class Parameter
{

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly string _name;
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly string _type;

    public string Name { get { return _name; } }
    public string Type { get { return _type; } }

    public Parameter(string name, Type type)
    {
        _name = name;
        _type = type.Name;
    }

    public override string ToString()
    {
        return string.Format("{0} {1}", Type, Name);
    }

}

现在,您想使用此SimpleContextGenerator

管理堆栈
public class SimpleContextGenerator : IInterceptor
{

    public void Intercept(IInvocation invocation)
    {
        OperationContextSynchronizationContext synchronizationContext = null;
        try
        {
            // Build the logical call stack by storing the current method being called
            // in our custom context stack.  Note that only calls made through tracked
            // interfaces end up on the stack, so we may miss some details (such as calls
            // within the implementing class).
            var stack = ContextStack.Current;
            Frame frame = new Frame(
                invocation.Request.Target.GetType(),
                invocation.Request.Method.Name,
                invocation.Request.Method.GetParameters().Select(param => new Parameter(param.Name, param.ParameterType)).ToArray());
            var dispose = stack.Push(frame);

            // Make sure that the OperationContext flows across to deeper calls,
            // since we need it for ContextStack.  (And also it's cool to have it.)
            synchronizationContext = new OperationContextSynchronizationContext(frame);

            // Process the method being called.
            try
            {
                invocation.Proceed();
            }
            catch (Exception ex)
            {
                frame.SetException(ex);
                throw;
            }

            var returnType = invocation.Request.Method.ReturnType;
            if (returnType == typeof(Task)
                || (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)))
            {
                Task task = invocation.ReturnValue as Task; // Could be a Task or a Task<>, and we honestly don't really care which.
                frame.SetTask(task);
                task.ContinueWith(t =>
                {
                    // If we've succeeded, then remove.
                    if (!t.IsFaulted)
                    {
                        dispose.Dispose();
                    }
                    else
                    {
                        frame.SetException(t.Exception);
                    }
                });
            }
            else
            {
                // If we're not returning a Task, that means that we've fully processed the method.
                // This will be hit for async void methods as well (which are, as far as we're
                // concerned, fully processed).
                dispose.Dispose();
            }
        }
        finally
        {
            //SynchronizationContext.SetSynchronizationContext(original);
            if (synchronizationContext != null)
            {
                synchronizationContext.Dispose();
            }
        }
    }

}

IInterceptor这里是Ninject.Extensions.Interception.IInterceptor

为了保持每次通话的OperationContext可用,您需要使用此OperationContextSynchronizationContext

public class OperationContextSynchronizationContext : SynchronizationContext, IDisposable
{

    // Track the operation context to make sure that it flows through to the next call context.

    private readonly Frame _currentFrame;
    private readonly OperationContext _context;
    private readonly SynchronizationContext _previous;

    public OperationContextSynchronizationContext(Frame currentFrame)
    {
        _currentFrame = currentFrame;
        _context = OperationContext.Current;
        _previous = SynchronizationContext.Current;
        SynchronizationContext.SetSynchronizationContext(this);
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        var context = _previous ?? new SynchronizationContext();
        context.Post(
            s =>
            {
                OperationContext.Current = _context;
                try
                {
                    d(s);
                }
                catch (Exception ex)
                {
                    // If we didn't have this, async void would be bad news bears.
                    // Since async void is "fire and forget," they happen separate
                    // from the main call stack.  We're logging this separately so
                    // that they don't affect the main call (and it just makes sense).

                     // implement your logging here
                }
            },
            state
        );
    }

    private bool _disposed = false;
    public void Dispose()
    {
        if (!_disposed)
        {
            // Return to the previous context.
            SynchronizationContext.SetSynchronizationContext(_previous);
            _disposed = true;
        }
    }
}

然后你只需要在Ninject绑定中将它全部挂钩:

Bind<IBusinessLayer>().To<BusinessLayer>()
    .Intercept().With<SimpleContextGenerator>(); // Track all logical calls.

请注意,这可以应用于接口到具体类绑定,这就是我们无法以这种方式将服务本身放入堆栈的原因。我们可以包装每个服务方法(比包装每个调用更好),但我认为我们甚至不能用模块来做,因为服务框架不会有堆栈遍历的异常(下面) )。

最后,在IErrorHandler

var context = ContextStack.Current.Stack;
if (context.Any())
{
    // Get all tasks that haven't yet completed and run them.  This will clear out any stack entries
    // that don't error.  This will run at most once; there should not be any situation where it
    // would run more than once.  As such, not doing a loop, though, if we find a situation where it
    // SHOULD run more than once, we should put the loop back in (but with a check for max loops).
    var frames = context.Where(frame => frame.Task != null && !frame.Task.IsCompleted);
    //while (tasks.Any())
    //{
        foreach (var frame in frames.ToList()) // Evaluate to prevent the collection from being modified while we're running the foreach.
        {
            // Make sure that each task has completed.  This may not be super efficient, but it
            // does allow each method to complete before we log, meaning that we'll have a good
            // indication of where all the errors are, and that seems worth it to me.
            // However, from what I've seen of the state of items that get here, it doesn't look
            // like anything here should error.
            try
            {
                frame.Task.Wait();
            }
            catch (Exception taskEx)
            {
                frame.SetException(taskEx);
            }
        }
    //}
}

// Prepare error information for one or more errors.
// Always use the frames instead of the one that got us here,
// since we have better information in the frames.

var errorFrames = context.Where(frame => frame.Exception != null);
if (errorFrames.Any())
{
    // Unpack all exceptions so we have access to every actual exception in each frame.
    var unpackedErrorFrames = errorFrames.GroupBy(frame => frame.Exception.Unpack())
                                         .Select(group => new { Frame = group.First(), Exceptions = group.Key });

    // Expand out the exceptions.
    var expandedFrames = (from frame in unpackedErrorFrames
                          from exception in frame.Exceptions
                          select new { Frame = frame.Frame, Exception = exception });

    // Walk the stack.
    // The stack does not currently have the service itself in it, because I don't have an easy way to
    // wrap the service call to track the service frame and exception..
    var errorStacks = expandedFrames.GroupBy(frame => frame.Exception)
                                    .Select(group => new { Exception = group.Key, Stack = group.ToList() });

    // Log all exceptions.
    foreach (var stack in errorStacks)
    {
        var exception = stack.Exception;
        var @class = stack.Stack.First().Type;
        var method = stack.Stack.First().Method;
        var exceptionStack = stack.Stack.SelecT(s => s.Frame);
        // log exception here.
    }
}
else
{
    // Well, if we don't have any error frames, but we still got here with an exception,
    // at least log that exception so that we know.
    // Since the service itself isn't in the stack, we'll get here if there are any
    // exceptions before we call the business layer.

    // log error here
}

这是Unpack扩展方法:

public static IEnumerable<Exception> Unpack(this Exception exception)
{
    List<Exception> exceptions = new List<Exception>();
    var agg = exception as AggregateException;
    if (agg != null)
    {
        // Never add an AggregateException.
        foreach (var ex in agg.InnerExceptions)
        {
            exceptions.AddRange(ex.Unpack());
        }
    }
    else
    {
        exceptions.Add(exception);
    }
    return exceptions;
}