web-dev-qa-db-fra.com

Durée de vie des applications AppDomain et MarshalByRefObject: comment éviter une RemotingException?

Lorsqu'un objet MarshalByRef est passé d'un AppDomain (1) à un autre (2), si vous attendez 6 minutes avant d'appeler une méthode dessus dans le second AppDomain (2), vous obtiendrez une exception RemotingException:

System.Runtime.Remoting.RemotingException: L'objet [...] a été déconnecté ou n'existe pas sur le serveur.

Quelques documentations sur cette isse:

Corrigez-moi si je me trompe: si InitializeLifetimeService renvoie la valeur null, l'objet ne peut être collecté dans AppDomain 1 que si AppDomain 2 est déchargé, même si le proxy a été collecté.

Existe-t-il un moyen de désactiver la durée de vie et de conserver le proxy (dans AppDomain 2) et l'objet (dans AppDomain1) jusqu'à la finalisation du proxy? Peut-être avec ISponsor ...?

55
Guillaume

voir la réponse ici:

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

qui dit essentiellement:

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

J'ai finalement trouvé un moyen de faire des instances activées par le client mais cela implique du code managé dans Finalizer: punaise.

Les deux classes suivantes doivent figurer dans un assembly chargé dans tous les domaines d'application impliqués.

  /// <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));
    }
  }

Et maintenant, CrossAppDomainObject, votre objet distant doit hériter de cette classe au lieu de 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);
    }
  }
13
Guillaume

Malheureusement, cette solution est incorrecte lorsque AppDomains est utilisé à des fins de plug-in (l'assemblage du plug-in ne doit pas être chargé dans votre principal domaine d'application).

L'appel de GetRealObject () dans votre constructeur et votre destructeur a pour résultat l'obtention du type réel de l'objet distant, ce qui conduit à essayer de charger l'assembly de l'objet distant dans le AppDomain actuel. Cela peut provoquer une exception (si l'assembly ne peut pas être chargé) ou l'effet indésirable de charger un assemblage étranger que vous ne pourrez pas décharger ultérieurement.

Une meilleure solution peut être si vous enregistrez vos objets distants dans votre méthode AppDomain principale avec ClientSponsor.Register () (non statique, vous devez donc créer une instance de sponsor client). Par défaut, il renouvellera vos mandataires distants toutes les 2 minutes, ce qui suffit si la durée de vie de vos objets est de 5 minutes par défaut.

6
taffer

J'ai créé une classe qui se déconnecte lors de la destruction.

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

    ~MarshalByRefObjectPermanent()
    {
        RemotingServices.Disconnect(this);
    }
}
1
Squall Leonhart

Il y a deux solutions possibles ici. 

L'approche Singleton: Ignorer InitializeLifetimeService

Comme Sacha Goldshtein souligne dans l'article du blog lié à l'affiche d'origine, si votre objet Marshaled a la sémantique Singleton, vous pouvez remplacer la variable 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;
    }
}

Cependant, comme le fait remarquer l’utilisateur 266748 dans une autre réponse } _ 

cette solution ne fonctionnerait pas si un tel objet était créé à chaque fois un client se connecte, car ils ne seraient jamais récupérés et votre la consommation de mémoire augmenterait jusqu'à ce que vous arrêtiez votre serveur ou il se bloque parce qu'il n'a plus de mémoire

L'approche basée sur les classes: Utilisation de ClientSponsor

Une solution plus générale consiste à utiliser ClientSponsor pour prolonger la durée de vie d'un objet distant activé par la classe. L'article MSDN lié contient un exemple de départ utile que vous pouvez suivre:

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();
      }
   }
}

La gestion de la durée de vie dans l'API Remoting ne vaut rien, elle est très bien décrite ici sur MSDN . J'ai cité la partie que j'ai trouvée la plus utile: 

Le service de durée de vie à distance associe un contrat de location à chaque service, et supprime un service lorsque la durée de son bail expire. La vie le service peut prendre la fonction d'une poubelle distribuée traditionnelle collecteur, et il s’ajuste également bien lorsque le nombre de clients par serveur augmente.

Chaque domaine d'application contient un gestionnaire de bail responsable pour contrôler les baux dans son domaine. Tous les contrats de location sont examinés périodiquement pour les durées de bail expirées. Si un bail a expiré, un ou plusieurs des sponsors du contrat de location sont invoqués et ont la possibilité de renouveler le bail. Si aucun des sponsors ne décide de renouveler le bail, le responsable du bail supprime le bail et l'objet peut être collecté par le ramasse-miettes. Le responsable du bail gère une liste de bail avec baux triés par durée de bail restante. Les baux avec le plus court le temps restant est stocké en haut de la liste. Le remoting le service à vie associe un bail à chaque service et supprime un service lorsque sa durée de bail expire. 

0
cdiggins

Si vous souhaitez recréer l'objet distant après qu'il a été collecté sans avoir à créer une classe ISponsor ni à lui donner une durée de vie infinie, vous pouvez appeler une fonction fictive de l'objet distant tout en interceptant 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);
    }
}
0
caiosm1005
[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
public override object InitializeLifetimeService()
{
  return null;
}

J'ai testé celui-ci et son fonctionnement fonctionne bien, bien sûr, il faut savoir que le mandataire vit pour toujours, jusqu'à ce que vous effectuiez la GC-ing pour vous-même. Mais dans mon cas, en utilisant un Plugin-Factory connecté à mon application principale, il n'y a pas de fuite de mémoire ou quelque chose comme ça. Je me suis assuré que j'implémente IDisposable et que tout fonctionne bien (je peux le dire, car mes dll chargées (dans l'usine) peuvent être écrasées une fois que l'usine est correctement éliminée)

Edit: Si vos événements de propagation dans les domaines, ajoutez cette ligne de code à la classe créant également le proxy, sinon votre diffusion sera également lancée;)

0
stephan Schmuck

Vous pouvez essayer un objet ISponsor singleton sérialisable implémentant IObjectReference. L'implémentation GetRealObject (d'IObjectReference doit renvoyer MySponsor.Instance lorsque context.State est CrossAppDomain, sinon elle-même renvoie. MySponsor.Instance est une auto-initialisation, synchronisée (MethodImplOptions.Synchronized), singleton. static MySponsor.IsFlaggedForUnload et renvoie TimeSpan.Zero lorsqu'il est marqué pour unload/AppDomain.Current.IsFinalizingForUnload () ou renvoie LifetimeServices.RenewOnCallTime sinon.

Pour l'attacher, procurez-vous simplement un ILease and Register (MySponsor.Instance), qui sera transformé en l'ensemble MySponsor.Instance dans AppDomain en raison de l'implémentation GetRealObject.

Pour arrêter le parrainage, obtenez à nouveau ILease et Unregister (MySponsor.Instance), puis définissez MySponsor.IsFlaggedForUnload via un rappel inter-AppDomain (myPluginAppDomain.DoCallback (MySponsor.FlagForUnload)).

Cela devrait maintenir votre objet en vie dans l'autre AppDomain jusqu'à l'appel non-inscrit, l'appel FlagForUnload ou le déchargement AppDomain.

0
Paul Pervinkler