I have a command object, doing work based on a request from a request queue. This particular command will execute its work in a child appdomain. Part of doing its work in the child appdomain involves blocking on a ConcurrentQueue operation (eg, Add or Take). I need to be able to propagate an abort signal through the request queue, across to the child appdomain, and to wake up the worker threads therein.
Therefore, I think I need to pass a CancellationToken across the AppDomain boundary.
I tried creating a class which inherits from MarshalByRefObject:
protected class InterAppDomainAbort : MarshalByRefObject, IAbortControl
{
public InterAppDomainAbort(CancellationToken t)
{
Token = t;
}
[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
public override object InitializeLifetimeService()
{
return null;
}
public CancellationToken Token
{
get;
private set;
}
};
and passing this as an argument on the worker function:
// cts is an instance variable which can be triggered by another thread in parent appdomain
cts = new CancellationTokenSource();
InterAppDomainAbort abortFlag = new InterAppDomainAbort(cts.Token);
objectInRemoteAppDomain = childDomain.CreateInstanceAndUnwrap(...);
// this call will block for a long while the work is being performed.
objectInRemoteAppDomain.DoWork(abortFlag);
But I still get an exception when the objectInRemoteAppDomain tries to access the Token getter property:
System.Runtime.Serialization.SerializationException: Type 'System.Threading.CancellationToken' in Assembly 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' is not marked as serializable.
My question is: How can I propagate the abort/cancellation signal across the appdomains and wake up threads that may be blocked in .NET concurrency data structures (where CancellationToken arguments are supported).
It's been a while since I looked at any cross-AppDomain stuff, so there might be problems with this code that I haven't realised, but it seems to do the job. The fundamental problem is that there seems no way to transfer a CancellationToken[Source] from one AppDomain to another. So I create two sources, with the primary set up to cancel the secondary when appropriate.
The fact that there are two separate token sources in this scenario could of course be a problem, but I don't think you're getting around the fact that lack of serialisability prevents you from using the same one in two separate AppDomains anyway.
Standard caveats about minimal error-checking, Dispose
implementations, etc.
// I split this into a separate interface simply to make the boundary between
// canceller and cancellee explicit, similar to CancellationTokenSource itself.
public interface ITokenSource
{
CancellationToken Token { get; }
}
public class InterAppDomainCancellable: MarshalByRefObject,
ITokenSource,
IDisposable
{
public InterAppDomainCancellable()
{
cts = new CancellationTokenSource();
}
public void Cancel() { cts.Cancel(); }
// Explicitly implemented to make it less tempting to call Token
// from the wrong side of the boundary.
CancellationToken ITokenSource.Token { get { return cts.Token; } }
public void Dispose() { cts.Dispose(); }
private readonly CancellationTokenSource cts;
}
// ...
// Crucial difference here is that the remotable cancellation source
// also lives in the other domain.
interAppDomainCancellable = childDomain.CreateInstanceAndUnwrap(...);
var primaryCts = new CancellationTokenSource();
// Cancel the secondary when the primary is cancelled.
// CancellationToken.Register returns a disposable object which unregisters when disposed.
using (primaryCts.Token.Register(() => interAppDomainCancellable.Cancel()))
{
objectInRemoteAppDomain = childDomain.CreateInstanceAndUnwrap(...);
// DoWork expects an instance of ITokenSource.
// It can access Token because they're all in the same domain together.
objectInRemoteAppDomain.DoWork(interAppDomainCancellable);
// ... some other work which might cancel the primary token.
}
There is actually a much easier way to overcome this obstacle assuming your proxy type is a single responsibility. I am assuming of course that you maintain a collection of your created domains and unload them should your application be closed or your containing object be disposed. I also assume the reason you need the cancellation token is to cancel some async operation in your marshaled reference type.
You simply need to do the following:
Create your tokenSource and token fields and initialize them in your constructor.
_cancellationTokenSource = new CancellationTokenSource();
_token = _cancellationTokenSource.Token;
Subscribe to the following events. The UnhandledException will serve the purpose of catching any faulting exception which causes your domain to close prematurely. This should be a best practice.
var currDomain = AppDomain.CurrentDomain;
currDomain.DomainUnload += currDomain_DomainUnload;
currDomain.UnhandledException += currDomain_UnhandledException;
Call cancel on your token source when the domain unload event is called. Additionally you may want to have a dispose method that unsubscribes to the domain events that gets called from either or just let the domain cleanup process garbage collection.
void currDomain_DomainUnload(object sender, EventArgs e)
{
_log.Debug(FormatLogMessage(_identity, "Domain unloading Event!"));
_cancellationTokenSource.Cancel();
_logPlayer.Dispose();
}
void currDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
_log.Error(string.Format("***APP Domain UHE*** Error:{0}", e.ExceptionObject);
_cancellationTokenSource.Cancel();
_logPlayer.Dispose();
}