Tuesday, July 26, 2011

Isolating MEF components

What Managed Extensibility Framework does well is component discovery and composition (when saying component I usually refer to MEF part). This is not a big surprise as it was designed to do so. However, one particular area where MEF has nothing to say about is component isolation. What is it ? Component isolation allows for plugins to work together without impacting each other. If one component fails, the rest of the system remains stable as nothing has ever happened. Yes, I know – when designing custom software solutions, we rarely care about plugins (too bad), but about component isolation, we don’t care at all. Why ? Most notably because dealing with component isolation is not an easy task. Since the best way to isolate components is to host them in a separate process, there has to be a fast IPC mechanism, and there is no single address space. Because we have calls across process boundaries, we have to serialize arguments on one side, deserialize them on the other, etc. Quite a bit of work there.

Some of you are probably familiar with System.AddIn namespace, better known as Managed Addin Framework. It has shipped long time ago, in .NET 3.0 if I remember well. This framework provides robust plugin infrastructure, and yes, it supports component isolation on App Domain and Process levels (okay there is also no isolation if you want to leverage plugins only). There is one problem with MAF, though. It has steep learning curve making it hard to jump into it. I never heard someone is using it in a production environment. Alternative ? Haven’t heard about yet. If you had, drop me a line please :)

Recently I have been thinking about creating custom component isolation mechanism and integrating it with Managed Extensibility Framework. Although not that straightforward, I managed to create special, dedicated catalog which can instantiate MEF parts in a separate AppDomain or even a separate process!

Meet the IsolatingCatalog

The IsolatingCatalog is responsible for doing all the magic. It is a decorating catalog, meaning it accepts another catalog, scans all its parts looking for those which need to be isolated, and then instantiates the parts accordingly. The API for the catalog is extremely simple. All you have to do is to tell MEF that a part has to be isolated. To do so, you can use custom export attribute or custom metadata attribute assuming you’re doing the export your way. Here is an example:

[IsolatedExport(typeof(IFakePart), Isolation = IsolationLevel.Process)]
public class FakePart1 : IFakePart
{
}

IsolatedExportAttribute is a custom export attribute which adds some metadata to the exported part, which is interpreted by the IsolatingCatalog so that it knows what to do with the export. The core property is the Isolation property of type IsolationLevel. Not surprisingly, it defines the isolation level for the part :) And the isolation level can be None (mostly used for testing only), AppDomain, and Process. I guess they are self explanatory. As I said, you can also use IsolatedAttribute, which is a custom metadata attribute.

[Export(typeof(IFakePart)), Isolated(Isolation = IsolationLevel.None)]
public class DisposableFakePart1 : IFakePart, IDisposable
{
}

What else can you define for an isolated export ? All properties are specified in the IIsolationMetadata interface which is defined as follows:

public interface IIsolationMetadata
{
/// <summary>
/// Gets the isolation for a part.
/// </summary>
IsolationLevel Isolation { get; }

/// <summary>
/// Indicates if a new host should be created for a new instance of the part.
/// </summary>
bool HostPerInstance { get; }

/// <summary>
/// Gets the name of the isolation group. Used to make sure parts which
/// defined the same name will be hosted in the same activation host.
/// </summary>
string IsolationGroup { get; }
}

With HostPerInstance property you can instruct the runtime to create separate host each time a part is created. I will cover hosts in much more details later on, right now a host is an activation host which, heh, hosts the instance of the created part. And IsolationGroup property is nothing more than a name for the activation host. If two or more parts specify the same name, they will be hosted in the same activation host, be in a separate AppDomain or a process. Note this interface is implemented by both the custom export and custom metadata attribute ensuring you can achieve the same results using either of the approaches.

Last thing to cover, although pretty straightforward, is how to use the catalog itself. As I said, it is a decorating catlog, and there is no philosophy is setting it up:

var typeCatalog = new TypeCatalog(typeof(TempPart), typeof(FakePart1));
var isolatingCatalog = new IsolatingCatalog(typeCatalog);
var container = new CompositionContainer(isolatingCatalog);

var part = container.GetExportedValue<TempPart>();

Let’s stop here for one second, and let’s assume FakePart1 is instantiated in isolation, with TempPart consuming it:

[Export]
public class TempPart
{
[Import]
public IFakePart Part { get; set; }
}

What gets injected into Part property ? If we run this code in debug mode, we would see this:

FakePartProxy

This is very important. What gets injected is a proxy supporting the IFakePart contract rather than a concrete FakePart1 implementation. Why ? Of course because the original FakePart1 is instantiated in isolation, typically in a separate process or AppDomain, so what the client gets is a proxy which serializes all calls to the concrete implementation. Seems obvious, but I wanted to stress it :)

Demo

Now that you have seen how to make isolated parts, it’s time for a little demo. I put a simple app (I took a simple MDI sample from Caliburn project) which displays “movies” from various providers. It shouldn’t be a surprise that each movie provider is a MEF part, and some of them are working properly, and some of them are not.

image

Lets have a quick look at how each provider is exported:

[IsolatedExport(typeof(IMovieProvider), HostPerInstance = true, Isolation = IsolationLevel.Process, IsolationGroup = "SomeName")]
public class Provider1 : IMovieProvider
{
}

You can see that each provider is instantiated in a separate, named activation host. However, because all providers specify the same IsolationGroup, there will be a separate process per each three instances of the providers. Also, the app supports tabbed interface, with each tab getting its own set of providers. This means each tab will have its own process hosting the providers, which can easily be verified in the Task Manager (three tabs above means three processes below):

TaskManager

This mimics the Google Chrome browser approach, where each tab is hosted in a separate process. Okay, lets break something. Provider 3, when executed 2 times, will throw an exception on a separate thread. This is of course by design :) What will happen ? Normally, the application would crush and the process would terminate. However, because the logic which will fail is hosted in a separate process, that process would crush leaving the main app intact. As if this was not enough, the API allows to intercept failures, giving the ability to display error to the user:

image

As you see, even though there was a critical error in one of the providers, the application remains stable. If we go to the Task Manager we will see two processes hosting the providers, as the third one just failed. Btw, make sure you manually copy PluginContainer.exe (this guy can host MEF parts in it) to the bin folder. In feature releases, I will link this assembly directly to IsolatingCatalog.

Lack of features

Although working, this solution is not perfect. One of the drawbacks is that if a developer wants to isolate a part, it has to be exported accordingly. I think it would be nice to have the ability to setup isolation on the import level, so that even though the part developer didn’t plan for isolation, the import side would switch the isolation on. I imagine there would be IsolatedImportAttribute which could do the trick. The other thing is I’m using WCF for IPC. At first it seems okay, but creating WCF host is a costly operation, thus you will notice a slight delay when opening new tabs in the above demo. Perhaps it would be good to craft custom IPC based on named pipes ? Last thing is lack of dependency injection, since we might operate in a different address space. The good news is that it should be possible to address this; WCF supports duplex channel communication on named pipes binding, so I could use proxy to communicate back with the host application. This is pending on my TODO list :)

Download

Although namespace suggests IsolatingCatalog is part of MefContrib, it is not. It’s not because this is a POC rather than something production ready. I wrote it mainly to prove it is possible to isolate MEF parts. Having said that, I think the approach chosen is good (inproc WCF with named pipes binding is used for the IPC purposes), and I would love to see this being part of MefContrib someday. The only thing is time needed to add more test cases, doing a cleanup, etc. If you feel you could help me with that, drop me a line and we can work together!

To download the IsolatingCatalog, just go to my MefContrib fork and download isolation branch. The demo presented above can be downloaded from my skydrive below.



Conclusion

This post covered a working solution which enables to activate MEF parts in a separate AppDomain or even a separate process. Next post will cover how the isolation is being handled by IsolatingCatalog. Although not yet completed, my approach seems working fine, and after extensive testing I think it will make its way to the MefContrib. In the meantime, if you feel you could contribute to that catalog, either from MEF or isolation perspectives, fell free to contact me so that we might exchange ideas, etc. Hope you liked it!

4 comments:

Luis Antunes said...

Great article!

Since I start working with MEF, that I miss the isolation feature.
I think this a really good approach, congrats!

Do you know if there are intentions of the 'Part Isolation' feature is release in a future MEF version?

Piotr Włodek said...

As far as I know, no :( MEF originally wasn't designed with isolation in mind, and it will probably stay the way it is now.

Vincent said...
This comment has been removed by the author.
Sumeet Tayal said...

This is the great implementation, it helped me a lot but I am missing to have a ImportIsolation also added to the framework so that I can add remove parts on demand from the GUI. Also if I can use the same channel for communication between the host and the parts. Please advice.

Thanks