AppDomain and MarshalByRefObject life time : how to avoid RemotingException?
Asked Answered
G

10

64

When a MarshalByRef object is passed from an AppDomain (1) to another (2), if you wait 6 mins before calling a method on it in the second AppDomain (2) you will get a RemotingException :

System.Runtime.Remoting.RemotingException: Object [...] has been disconnected or does not exist at the server.

Some documentation about this isse :

Correct me if I'm wrong : if InitializeLifetimeService returns null, the object can only be collected in AppDomain 1 when AppDomain 2 is Unloaded, even if the proxy was collected ?

Is there a way to disable life time and keep the proxy (in AppDomain 2) and the object (in AppDomain1) alive until the proxy is Finalized ? Maybe with ISponsor... ?

Gaddy answered 9/3, 2010 at 15:36 Comment(0)
C
47

see answer here:

http://social.msdn.microsoft.com/Forums/en-US/netfxremoting/thread/3ab17b40-546f-4373-8c08-f0f072d818c9/

which basically says:

[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
public override object InitializeLifetimeService()
{
  return null;
}
Cele answered 24/5, 2011 at 13:21 Comment(7)
The object will remain connected and you will shortly run out of resources if you have many remoted objects. The second part of my question is about InitializeLifetimeService returning null.Gaddy
Actually, I have only one remoting object. The operation may take extremely long time to finish (based on user data, it can take days...). Using this implementation there is no running out of resources - I've tested and re-tested.Cele
Um... several people downvoted this without saying a word as to why they did so. While that may mean nothing at all, it would be nice to know why (from a civilization standpoint...). Also, this solution works very well in real life commercial application, I did not just pull it out of a hat.Cele
I guess the downvotes are because your solution is quite extreme. Sure it works in your real life commercial application, but only because you're not creating new objects over and over again. I use the same solution for 1 object that I know has to live forever until the app is closed. But that solution wouldn't work if such an object were created each time a client connects itself, because they would never be GCed and your memory consumption would go up and up until either you stop your server or it crashes because it has no more memory.Diablerie
I have "Answer Checker" modules that are dyamically compiled and recompiled when the source code changes. I use a separate app domain so that the modules can be unloaded and reloaded. If I have a hundred questions, each with their own module, and create a MarshalByRef object for each of them only once, would having a hundred such objects cause a server to run out of resources?Ochlophobia
@Triynko, you should create a new question linking this answserGaddy
I tested for my plugin system and this did not work. Remote instance has been garbage collected.@taffer was right.Togetherness
G
13

I finally found a way to do client activated instances but it involves managed code in Finalizer :( I specialized my class for CrossAppDomain communication but you may modify it and try in others remoting. Let me know if you find any bug.

The two following classes must be in an assembly loaded in all application domains involved.

  /// <summary>
  /// Stores all relevant information required to generate a proxy in order to communicate with a remote object.
  /// Disconnects the remote object (server) when finalized on local host (client).
  /// </summary>
  [Serializable]
  [EditorBrowsable(EditorBrowsableState.Never)]
  public sealed class CrossAppDomainObjRef : ObjRef
  {
    /// <summary>
    /// Initializes a new instance of the CrossAppDomainObjRef class to
    /// reference a specified CrossAppDomainObject of a specified System.Type.
    /// </summary>
    /// <param name="instance">The object that the new System.Runtime.Remoting.ObjRef instance will reference.</param>
    /// <param name="requestedType"></param>
    public CrossAppDomainObjRef(CrossAppDomainObject instance, Type requestedType)
      : base(instance, requestedType)
    {
      //Proxy created locally (not remoted), the finalizer is meaningless.
      GC.SuppressFinalize(this);
    }

    /// <summary>
    /// Initializes a new instance of the System.Runtime.Remoting.ObjRef class from
    /// serialized data.
    /// </summary>
    /// <param name="info">The object that holds the serialized object data.</param>
    /// <param name="context">The contextual information about the source or destination of the exception.</param>
    private CrossAppDomainObjRef(SerializationInfo info, StreamingContext context)
      : base(info, context)
    {
      Debug.Assert(context.State == StreamingContextStates.CrossAppDomain);
      Debug.Assert(IsFromThisProcess());
      Debug.Assert(IsFromThisAppDomain() == false);
      //Increment ref counter
      CrossAppDomainObject remoteObject = (CrossAppDomainObject)GetRealObject(new StreamingContext(StreamingContextStates.CrossAppDomain));
      remoteObject.AppDomainConnect();
    }

    /// <summary>
    /// Disconnects the remote object.
    /// </summary>
    ~CrossAppDomainObjRef()
    {
      Debug.Assert(IsFromThisProcess());
      Debug.Assert(IsFromThisAppDomain() == false);
      //Decrement ref counter
      CrossAppDomainObject remoteObject = (CrossAppDomainObject)GetRealObject(new StreamingContext(StreamingContextStates.CrossAppDomain));
      remoteObject.AppDomainDisconnect();
    }

    /// <summary>
    /// Populates a specified System.Runtime.Serialization.SerializationInfo with
    /// the data needed to serialize the current System.Runtime.Remoting.ObjRef instance.
    /// </summary>
    /// <param name="info">The System.Runtime.Serialization.SerializationInfo to populate with data.</param>
    /// <param name="context">The contextual information about the source or destination of the serialization.</param>
    public override void GetObjectData(SerializationInfo info, StreamingContext context)
    {
      Debug.Assert(context.State == StreamingContextStates.CrossAppDomain);
      base.GetObjectData(info, context);
      info.SetType(typeof(CrossAppDomainObjRef));
    }
  }

And now the CrossAppDomainObject, your remoted object must inherit from this class instead of MarshalByRefObject.

  /// <summary>
  /// Enables access to objects across application domain boundaries.
  /// Contrary to MarshalByRefObject, the lifetime is managed by the client.
  /// </summary>
  public abstract class CrossAppDomainObject : MarshalByRefObject
  {
    /// <summary>
    /// Count of remote references to this object.
    /// </summary>
    [NonSerialized]
    private int refCount;

    /// <summary>
    /// Creates an object that contains all the relevant information required to
    /// generate a proxy used to communicate with a remote object.
    /// </summary>
    /// <param name="requestedType">The System.Type of the object that the new System.Runtime.Remoting.ObjRef will reference.</param>
    /// <returns>Information required to generate a proxy.</returns>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public sealed override ObjRef CreateObjRef(Type requestedType)
    {
      CrossAppDomainObjRef objRef = new CrossAppDomainObjRef(this, requestedType);
      return objRef;
    }

    /// <summary>
    /// Disables LifeTime service : object has an infinite life time until it's Disconnected.
    /// </summary>
    /// <returns>null.</returns>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public sealed override object InitializeLifetimeService()
    {
      return null;
    }

    /// <summary>
    /// Connect a proxy to the object.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public void AppDomainConnect()
    {
      int value = Interlocked.Increment(ref refCount);
      Debug.Assert(value > 0);
    }

    /// <summary>
    /// Disconnects a proxy from the object.
    /// When all proxy are disconnected, the object is disconnected from RemotingServices.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public void AppDomainDisconnect()
    {
      Debug.Assert(refCount > 0);
      if (Interlocked.Decrement(ref refCount) == 0)
        RemotingServices.Disconnect(this);
    }
  }
Gaddy answered 11/3, 2010 at 8:19 Comment(11)
If I could upvote this more than once I would. It gave me a nice head-start solving a similar problem.Emiliaemiliaromagna
I wish I could upvote this 5 times. RemotingServices.Disconnect() is the key.Encumbrancer
This is wrong. You should be using an ISponsor from the parent AppDomain to manage the lifespan of the instance in the child AppDomain. That what MBRO is designed for. This is a COM-inspired hack.Marcimarcia
@Will I'm not an expert on remoting and I totally agree, this is a hack and I don't use it. ISponsor would require that I call Renewal() frequently ?Gaddy
@Guillaume: Its actually pretty easy to implement. You call InitializeLifetimeService on the proxy in the parent domain. It returns an object which you cast to ILease. You then call Register on the lease passing in an ISponsor. Every so often the framework will call Renewal on the ISponsor, and all you have to do is determine if you wish to renew the proxy and return an appropriate TimeSpan length.Marcimarcia
@Will So I should call InitializeLifetimeService on every single instance passed from one domain to another ?Gaddy
@Guillaume: You do it when you call CreateInstance(From)AndUnwrap. That's when you create the proxy, so the next step is to handle how long the proxy should stay connected to the instance in the other AppDomain.Marcimarcia
@Will ok but what about MBR objects returned by this proxy ? I'll have to to it with each instance of each object. I was working on an existing project on which theses modifications would have been too expensives. That's why I was trying a hack that automatically handles each instance.Gaddy
@Guillaume: Well, ya gotta do what ya gotta do. Its just important that people searching for this answer understand what's going on. Always returning null from MBRO.ILS is like always catching and swallowing Exception. Yes, there are times where you should do this, but only when you know exactly what you're doing.Marcimarcia
@Will: Thanks, I almost extract a solution from your comments. But Why don't you give a full, correct answer?Affluence
@sad_man: Its more complex than that, and depends on your design. What his answer would be might not cover what yours is. Consider asking a question specifying what you are trying to do and how you wish to use ISponsor/ILease to control the lifetime of objects in another appdomain.Marcimarcia
H
10

There are two possible solutions here.

The Singleton approach: Override InitializeLifetimeService

As Sacha Goldshtein points out in the blog post linked to by the original poster, if your Marshaled object has Singleton semantics you can override InitializeLifetimeService:

class MyMarshaledObject : MarshalByRefObject
{
    public bool DoSomethingRemote() 
    {
      // ... execute some code remotely ...
      return true; 
    }

    [SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
    public override object InitializeLifetimeService()
    {
      return null;
    }
}

However, as user266748 points out in another answer

that solution wouldn't work if such an object were created each time a client connects itself, because they would never be GCed and your memory consumption would go up and up until either you stop your server or it crashes because it has no more memory

The Class-Based approach: Using ClientSponsor

A more general solution is to use ClientSponsor to extend the life of a class-activated remote object. The linked MSDN article has a useful starting example you can follow:

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Remoting.Lifetime;
namespace RemotingSamples
{

   class HelloClient
   {
       static void Main()
      {
         // Register a channel.
         TcpChannel myChannel = new TcpChannel ();
         ChannelServices.RegisterChannel(myChannel);
         RemotingConfiguration.RegisterActivatedClientType(
                                typeof(HelloService),"tcp://localhost:8085/");

         // Get the remote object.
         HelloService myService = new HelloService();

         // Get a sponsor for renewal of time.
         ClientSponsor mySponsor = new ClientSponsor();

         // Register the service with sponsor.
         mySponsor.Register(myService);

         // Set renewaltime.
         mySponsor.RenewalTime = TimeSpan.FromMinutes(2);

         // Renew the lease.
         ILease myLease = (ILease)mySponsor.InitializeLifetimeService();
         TimeSpan myTime = mySponsor.Renewal(myLease);
         Console.WriteLine("Renewed time in minutes is " + myTime.Minutes.ToString());

         // Call the remote method.
         Console.WriteLine(myService.HelloMethod("World"));

         // Unregister the channel.
         mySponsor.Unregister(myService);
         mySponsor.Close();
      }
   }
}

It is worth nothing how lifetime management works in the Remoting API, which is described quite well here on MSDN. I've quoted the part I found most useful:

The remoting lifetime service associates a lease with each service, and deletes a service when its lease time expires. The lifetime service can take on the function of a traditional distributed garbage collector, and it also adjusts well when the numbers of clients per server increases.

Each application domain contains a lease manager that is responsible for controlling leases in its domain. All leases are examined periodically for expired lease times. If a lease has expired, one or more of the lease's sponsors are invoked and given the opportunity to renew the lease. If none of the sponsors decides to renew the lease, the lease manager removes the lease and the object can be collected by the garbage collector. The lease manager maintains a lease list with leases sorted by remaining lease time. The leases with the shortest remaining time are stored at the top of the list. The remoting lifetime service associates a lease with each service, and deletes a service when its lease time expires.

Heritor answered 27/1, 2019 at 17:29 Comment(2)
This answer is underratedLinguini
Microsoft introduced the ClientSponsor class to replace SponsporshipManager (original class). The undocumented problem is that Sponsor also has a lease, so when it gets expired, it can no longer respond to renewal requests. ClientSponsor creates itself with a non-expiring lease, so it sticks around to renew the sponsored object(s) as expected. Also undocumented is that ClientSponsor can register multiple objects.Kutz
G
7

Unfortunately this solution is wrong when AppDomains are used for plugin purposes (assembly of the plugin must not be loaded into your main appdomain).

The GetRealObject() call in your constructor and destructor results in obtaining the real type of the remote object, which leads to trying to load the assembly of the remote object into the current AppDomain. This may cause either an exception (if the assembly cannot be loaded) or the unwanted effect that you have loaded a foreign assembly that you cannot unload later.

A better solution can be if you register your remote objects in your main AppDomain with ClientSponsor.Register() method (not static so you must create a client sponsor instance). By default it will renew your remote proxies in every 2 minutes, which is enough if your objects has the default 5 minutes lifetime.

Goglet answered 16/4, 2010 at 16:17 Comment(2)
I added base.TypeInfo.TypeName = typeof(CrossAppDomainObject).AssemblyQualifiedName; in CrossAppDomainObjRef ctor but it still fails in some cases moreover ref counting can lead to leak on circular references...Gaddy
I tested and I confirm that. It does not work for a plugin mechanism.Togetherness
R
2

For those looking for a deeper understanding of the .NET Remoting Framework, I suggest the article titled "Remoting Managing the Lifetime of Remote .NET Objects with Leasing and Sponsorship" published in MSDN Magazine December 2003 issue.

Rutilant answered 31/10, 2019 at 2:12 Comment(1)
Yeah, we've read that. It doesn't have information about why the sponsor doesn't get called to renew the lease.Kutz
A
1
[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
public override object InitializeLifetimeService()
{
  return null;
}

I've tested this one and its working fine, of course one has to know that the proxy lives forever, until you do GC-ing for yourself. But i my case, using a Plugin-Factory connected to my main app, there is no memory leak or something like this. I just made sure, that i'm implementing IDisposable and it's working fine (I can tell, because my loaded dll's (in the factory) can be overwriten once the factory is disposed correctly)

Edit: If your bubbling events through the domains, add this line of code to the Class creating the proxy as well, otherwise your bubbling will throw too ;)

Astral answered 13/10, 2015 at 9:59 Comment(0)
S
1

I created a class which disconnect on destruction.

public class MarshalByRefObjectPermanent : MarshalByRefObject
{
    public override object InitializeLifetimeService()
    {
        return null;
    }

    ~MarshalByRefObjectPermanent()
    {
        RemotingServices.Disconnect(this);
    }
}
Secunda answered 24/2, 2016 at 3:28 Comment(0)
U
1

If you would like to re-create the remote object after it has been garbage collected without having to create an ISponsor class nor giving it infinite lifetime, you could call a dummy function of the remote object while catching RemotingException.

public static class MyClientClass
{
    private static MarshalByRefObject remoteClass;

    static MyClientClass()
    {
        CreateRemoteInstance();
    }

    // ...

    public static void DoStuff()
    {
        // Before doing stuff, check if the remote object is still reachable
        try {
            remoteClass.GetLifetimeService();
        }
        catch(RemotingException) {
            CreateRemoteInstance(); // Re-create remote instance
        }

        // Now we are sure the remote class is reachable
        // Do actual stuff ...
    }

    private static void CreateRemoteInstance()
    {
        remoteClass = (MarshalByRefObject)AppDomain.CurrentAppDomain.CreateInstanceFromAndUnwrap(remoteClassPath, typeof(MarshalByRefObject).FullName);
    }
}
Ulphi answered 8/10, 2016 at 22:50 Comment(1)
Hum, it's a pretty dirty solution.Togetherness
A
0

You could try a serializable singleton ISponsor object implementing IObjectReference. The GetRealObject implementation (from IObjectReference should return MySponsor.Instance when context.State is CrossAppDomain, otherwise return itself. MySponsor.Instance is a self-initializing, synchronized (MethodImplOptions.Synchronized), singleton. The Renewal implementation (from ISponsor) should check a static MySponsor.IsFlaggedForUnload and return TimeSpan.Zero when flagged for unload/AppDomain.Current.IsFinalizingForUnload() or return LifetimeServices.RenewOnCallTime otherwise.

To attach it, simply obtain an ILease and Register(MySponsor.Instance), which will be transformed into the MySponsor.Instance set within the AppDomain due to the GetRealObject implementation.

To stop sponsorship, re-obtain the ILease and Unregister(MySponsor.Instance), then set the MySponsor.IsFlaggedForUnload via a cross-AppDomain callback (myPluginAppDomain.DoCallback(MySponsor.FlagForUnload)).

This should keep your object alive in the other AppDomain until either the unregister call, the FlagForUnload call, or AppDomain unload.

Andros answered 9/6, 2014 at 20:56 Comment(0)
A
-2

I recently ran into this exception also. Right now my solution is just unload AppDomain and then reload AppDomain after a long interval. Luckily this temporary solution work for my case. I wish there is a more elegant way to deal with this.

Aesculapian answered 24/1, 2011 at 18:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.