Monday, June 7, 2010

CallContext Data Slots, CallContext


CallContext - удобный способ обмена информации между клиентом и сервером.
CallContext записывает данные в Thread Local Storage.


Чтобы для каждого потока информация была уникальной, необходимо использовать методы
SetData и GetData, но передаваться через границы AppDomain они будут только в том случае, если унаследованы от интерфейса ILogicalThreadAffinative.
Объекты, сохраненные через метод LogicalSetData, будут передаваться через границы домена даже в том случае, если они не унаследованы от ILogicalThreadAffinative.

Logical[Get/Set]Data добавляет/удаляет значения из Hashtable. 
Hashtable живёт в инстансе класса LogicalCallContext, который, в свою очередь, живёт внутри ExecutionContext, а тот — внутри Thread. Чтобы не разматывать весь стек: при переходе в новый контекст вызывается метод LogicalCallContext.Clone, а при возврате — LogicalCallContext.Merge.
Merge заменяет значения в Hashtable теми, что были установлены в новом контексте. 

И может выполниться в другом потоке, если EndInvoke был вызван асинхронно В результате значение вот этого абсолютно непредсказуемо:
 
Stack<int> stack = (Stack<int>)(CallContext.LogicalGetData("MyContext"));
 
Вот дополнительная информация:

The LogicalCallContext is able to flow bi-directionally through an async invocation or a .net remoting call. When you call EndInvoke, the child context's LogicalCallContext is merged back into the parent's, as you have observed. This is intentional, so that callers of remote methods can get access to any values set by the remote method. You can use this feature to flow data back from the child, if you'd like.
Debugging this with the help of the .NET Framework source stepping, there are explicit comments to this effect:

in System.Runtime.Remoting.Proxies.RemotingProxy.Invoke:
    case Message.EndAsync: 
         // This will also merge back the call context
         // onto the thread that called EndAsync
         RealProxy.EndInvokeHelper(m, false);
in System.Runtime.Remoting.Proxies.RealProxy.EndInvokeHelper:
    // Merge the call context back into the thread that
    // called EndInvoke 
    CallContext.GetLogicalCallContext().Merge(
         mrm.LogicalCallContext);
 
If you want to avoid having the data merge, it's pretty easy to skip, just avoid calling EndInvoke from the main thread. You could for example use ThreadPool.QueueUserWorkItem, which will flow the LogicalCallContext in but not out, or call EndInvoke from an AsyncCallback.
Looking at the example on the Microsoft Connect site, the reason that you're not seeing the LogicalSetData value get flowed back from the RunWorkerCompleted call is that BackgroundWorker does not flow the context back. Also, remember that LogicalSetData is not the same as thread-local storage, so it doesn't matter that RunWorkerCompleted happens to be running on the UI thread -- the LogicalCallContext there is still a child context, and unless the parent explicitly flows it back by calling EndInvoke from the spawning thread, it will be abandoned. If you want thread-local storage, you can access that from Thread, like so:

    private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        Thread.SetData(Thread.GetNamedDataSlot("foo"), "blah!!");
    }

    private void button1_Click(object sender, EventArgs e)
    {
        var val = (string)Thread.GetData(Thread.GetNamedDataSlot("foo"));
        MessageBox.Show(val ?? "no value");
    }

(from: http://dotnetmustard.blogspot.com/2008/08/identifying-differences-between.html)

Recently I was working on a server side caching strategy for permissions data. The key to the strategy was to place a user's permissions returned from a database call into the CallContext of WCF service. The code to do this looked something like this...

CallContext.SetData("permissionsKey", myPermissionsData);

All subsequent requests for permissions data would then be returned from the CallContext's cache permissions, thereby saving me from having to make redundant database calls for data that I already have.

myPermissionsData = (PermissionData)CallContext.LogicalGetData("permissionsKey")

Simple caching strategy...

During my testing I found that my cached data was not being returned. After further investigation I realized that I was setting the data using the CallContext's SetData method, but I was getting the data using the CallContext's LogicalGetData method. Come to find out, these are not the same. Seems there is a LogicalGetData method, LogicalSetData method, GetData method and SetData method on the CallContext object. So, it was a simple fix to use the LogicalSetData method in conjunction with the LogicalGetData method when implementing my caching strategy.

But why are there two methods that seemingly do the same thing? The documentation on MSDN doesn't really specify the differnce between the two.

Well... after doing some resarch and finding a post by Lucian Bargaoanu on the subject it seems that the difference has to do with AppDomains. Come to find out there is a LogicalCallContext and an IllogicalCallContext. LogicalCallContext will flow across appdomains. It will do this regardless of what type of object you have placed in context. The object doesn't have to implement ILogicalThreadAffinitive. When you call SetData with an ILogicalThreadAffinitive object, the data is set in the LogicalCallContext. When you call GetData it will first look in the LogicalCallContext and then in the IllogicalCallContext. You cannot have the same key in both CallContext(s).

In summary, objects stored using SetData will only flow across AppDomains if they implement ILogicalThreadAffinitive. Objects stored in LogicalSetData will flow across AppDomains even if they don't implement ILogicalThreadAffinitive. LogicalSetData handles seems to handle the ILogicalThreadAffinitive implementation for you.

0 коммент.:

Post a Comment

Powered by Blogger.