Introduction
Not too long ago, I posted an article about marshalling a remote client request from one AppDomain to another. You can read about it here.
I have since had some very interesting responses. Some of the issues raised were about loading and unloading of an AppDomain at runtime. That has motivated me to write this article about a simple component server that supports the installation and re-installation of components at runtime. The purpose of this article is to demonstrate that you can replace a component at runtime without causing any interruptions to the clients that rely upon the component's services.
The installable component
To make your component installable, you must add an installer class to your component's project. You must override the OnAfterInstall
method. Then, you must add a setup project to your Visual Studio solution and enable the custom actions. That having said, in a nutshell, I will explain how the installer co-operates with the component server to swap the components in and out while clients continue to make requests on that component.
The component server supports an interface for the installation and un-installation of a component.
public interface IComponentServer
{
void OnBeforeInstall(string objectUri, ComponentInstallInfo installInfo);
void OnAfterInstall(string objectUri, ComponentInstallInfo installInfo);
void OnBeforeUninstall(string objectUri);
void OnAfterUninstall(string objectUri);
}
The interface methods resemble the overridable event methods of the installer class. Indeed, the installer connects to the component server via IPC, and tells the component server to install or swap out a component. The objectUri
identifies the component, and the ComponentInstallInfo
provides the necessary information for the component server to do its work. Here is some insight into how the installer class accomplishes it:
IComponentServer server;
IComponentServer ComponentServer
{
get
{
const string url = "ipc://IpcLocal/ComponentServer.rem";
if (this.server == null)
this.server = (IComponentServer)
Activator.GetObject(typeof(IComponentServer), url);
return this.server;
}
}
ComponentInstallInfo installInfo;
ComponentInstallInfo InstallInfo
{
get
{
if (this.installInfo == null)
{
this.installInfo = new ComponentInstallInfo();
this.installInfo.TypeName = "ComponentServer.Component";
this.installInfo.AssemblyName = "Component";
this.installInfo.ConfigFileName = "Component.dll.config";
this.installInfo.ApplicationBase =
System.IO.Path.GetDirectoryName(
this.Context.Parameters["assemblyPath"]);
}
return this.installInfo;
}
}
string objectUri = "Component.rem";
protected override void OnAfterInstall(System.Collections.IDictionary savedState)
{
this.ComponentServer.OnAfterInstall(this.objectUri, this.InstallInfo);
}
Assuming that the component server is up and running, we can connect and communicate to it over the IPC channel. We pass the component's identifying objectUri
and a ComponentInstallInfo
. The data of the ComponentInstallInfo
is the information required for the creation of an AppDomain and for the instantiation of the component.
You need to build your components to be enabled for marshalling across an AppDomain. You can read about it in my other article.
In the next section, I shall explain how the component server manages the components.
The component server
The component server maintains a ComponentsManager
. The ComponentsManager
maintains a list of installed components that is keyed off the component's objectUri
. The CrossDomainMarhaller
gets a reference for a component from the ComponentManager
. You can find this in the CrossDomainMarshaller
's MessageSink
class.
public IMessage SyncProcessMessage(IMessage msg)
{
string objectUri = (string)msg.Properties["__Uri"];
if (objectUri == null)
return this.nextSink.SyncProcessMessage(msg);
else
{
try
{
return ComponentsManager.Instance().GetComponent(objectUri).Marshal(msg);
}
catch (Exception e)
{
return new ReturnMessage(e, (IMethodCallMessage)msg);
}
}
}
The ComponentsManager
's primary job is to install a component at runtime. I have designed it so that the creation of the AppDomain and the instantiation of the component is deferred to when a client's request is received for the first time. Here is the ComponentsManager.Install
method:
public void InstallComponent(string objectUri,
ComponentInstallInfo installInfo)
{
string key = "/" + objectUri;
ComponentInfo componentInfo = null;
if (!this.Components.ContainsKey(key))
{
componentInfo = new ComponentInfo();
componentInfo.InstallInfo = installInfo;
componentInfo.CrossDomainMarhaller = new CrossDomainMarshaller();
this.Components.Add(key, componentInfo);
RemotingServices.Marshal(componentInfo.CrossDomainMarhaller, objectUri);
Console.WriteLine("New component installed, {0}", objectUri);
return;
}
lock (this.SyncRoot)
{
componentInfo = this.Components[key];
if (componentInfo.AppDomain == null)
{
componentInfo.InstallInfo = installInfo;
Console.WriteLine("Ccomponent re-installed" +
" with new InstallInfo, {0}", objectUri);
return;
}
}
ComponentInfo newComponentInfo = new ComponentInfo();
newComponentInfo.InstallInfo = installInfo;
newComponentInfo.AppDomain = CreateAppDomain(objectUri,
installInfo.ApplicationBase, installInfo.ConfigFileName);
newComponentInfo.ComponentInstance = (ICrossDomainComponent)
newComponentInfo.AppDomain.CreateInstanceAndUnwrap(
installInfo.AssemblyName, installInfo.TypeName);
newComponentInfo.CrossDomainMarhaller =
componentInfo.CrossDomainMarhaller;
lock (SyncRoot)
{
this.Components[key] = newComponentInfo;
}
ThreadPool.QueueUserWorkItem(this.UnloadAppDomainThread,
componentInfo);
Console.WriteLine("Component replaced, {0}", objectUri);
}
There is a private class, ComponentInfo
, which serves only to contain various object references. Upon installing a component for the first time, a ComponentInfo
object is created and added to a collection of components. The interesting point to note here is that we are creating a new instance of a CrossDomainMarshaller
and publishing it to the component's objectUri
. This will allow a client to connect to the component via Activator.GetObject(typeof(IComponent), "tcp://localhost:1234/Component.rem");
. The reference of the CrossDomainMarshaller
is also assigned to the ComponentInfo
structure so that we can disconnect it properly when the component is uninstalled.
You may observe next that no AppDomain has been created yet. The AppDomain will be created only when a client connects for the first time. So, if you install the component one more time before the AppDomain is created, then only the install information is changed.
However, once a client has connected and made its first call into the component, then the install process is a replacement of the old component with the new one. We create a new AppDomain, instantiate the component in that new AppDomain, and assign everything to a new ComponentInfo
structure. Finally, the old ComponentInfo
structure is moved to be processed in a worker thread.
The big reason why the old ComponentInfo
is processed in a separate thread is that the AppDomain must not be unloaded until all client calls have ended. Here is the worker thread procedure:
void UnloadAppDomainThread(object item)
{
ComponentInfo info = (ComponentInfo)item;
while (info.ComponentInstance.Refcount > 0)
Thread.Sleep(1000);
AppDomain.Unload(info.AppDomain);
Console.WriteLine("AppDomain unloaded.");
}
The thread loops until the Refcount
decrements to zero. Implementing the component properly ensures that, for every client call, a reference count is incremented and decremented. You might want to download this article's source code and look at the implementation code.
Conclusion
What I have presented here is a component server for stateless components. I did not want to make the problem more complex than necessary. The model can support stateful components as well. That is a bit more complicated to implement because of the state transfer from the outgoing instance to the new instance. A component server for stateless installable components is nevertheless very useful.
In the interest of simplicity, I have omitted some important code. Firstly, the installer code should verify that the component server is running, before communicating with it. Secondly, when the component server exits, it should save the information about all installed components and read it back upon restart.
I hope that many of you will be inspired to develop something interesting along this concept of a component server for installable components. Please let me know about it and all the possible issues that you may encounter.
How to use the project
- First compile the entire solution.
- Start the component server.
- Install the components.
- Start the client.
Observe the console output of both the client and the server. While the client communicates with the server, modify the components so some different text can be printed to the Console. Now, install the modified components again. You should observe that the modified components take effect. And all of this without interrupting the client.
A note on the HTTP channel
One rather unexplained exception is thrown when the HTTP channel is used. It appears that the return message cannot be properly serialized by the SOAP formatter. It does work, however, when the HTTP channel is combined with the binary formatter.