Время жизни AppDomain и MarshalByRefObject: как избежать исключения RemotingException?
Когда объект MarshalByRef передается из AppDomain (1) другому (2), если вы будете ждать 6 минут, прежде чем вызывать метод для него во втором AppDomain (2), вы получите исключение RemotingException:
System.Runtime.Remoting.RemotingException: объект [...] отключен или не существует на сервере.
Некоторая документация по этому вопросу:
- http://blogs.microsoft.co.il/blogs/sasha/archive/2008/07/19/appdomains-and-remoting-life-time-service.aspx
- http://blogs.msdn.com/cbrumme/archive/2003/06/01/51466.aspx - время жизни экземпляра, cbrumme говорит: "Мы должны это исправить".:(
Поправьте меня, если я ошибаюсь: если InitializeLifetimeService возвращает ноль, объект может быть собран только в AppDomain 1, когда AppDomain 2 выгружен, даже если прокси был собран?
Есть ли способ отключить время жизни и сохранить прокси (в AppDomain 2) и объект (в AppDomain1) до тех пор, пока прокси не будет завершен? Может быть, с ISponsor...?
9 ответов
См ответ здесь:
который в основном говорит:
[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
public override object InitializeLifetimeService()
{
return null;
}
Наконец-то я нашел способ создания экземпляров, активированных клиентом, но он включает в себя управляемый код в Finalizer:(Я специализировал свой класс для связи через CrossAppDomain, но вы можете изменить его и попробовать в удаленном взаимодействии. Дайте мне знать, если найдете ошибку.
Два следующих класса должны находиться в сборке, загруженной во все задействованные домены приложения.
/// <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));
}
}
А теперь CrossAppDomainObject, ваш удаленный объект должен наследовать от этого класса вместо 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);
}
}
Здесь есть два возможных решения.
Подход Singleton: переопределить InitializeLifetimeService
Как указывает Саша Гольдштейн в сообщении в блоге, на которое ссылается оригинальный постер, если у вашего объекта Marshaled есть семантика Singleton, вы можете переопределить 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;
}
}
Тем не менее, как пользователь 266648 указывает в другом ответе
это решение не сработало бы, если бы такой объект создавался каждый раз, когда клиент сам подключается, потому что он никогда не будет GCed, а потребление вашей памяти будет расти до тех пор, пока вы не остановите свой сервер или не выйдет из строя, потому что у него больше нет памяти
Классовый подход: использование ClientSponsor
Более общее решение заключается в использовании ClientSponsor
продлить жизнь активированного классом удаленного объекта. В связанной статье MSDN есть полезный стартовый пример, которому вы можете следовать:
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();
}
}
}
Ничего не стоит, как работает управление временем жизни в Remoting API, который довольно хорошо описан здесь, в MSDN. Я процитировал ту часть, которая мне показалась наиболее полезной:
Служба удаленного взаимодействия связывает аренду с каждой службой и удаляет службу по истечении времени аренды. Служба на весь срок службы может взять на себя функцию традиционного распределенного сборщика мусора, а также хорошо настраивается при увеличении числа клиентов на сервер.
Каждый домен приложения содержит менеджер аренды, который отвечает за управление арендой в своем домене. Все договоры аренды периодически проверяются на предмет истекших сроков аренды. Если срок аренды истек, вызывается один или несколько спонсоров аренды и предоставляется возможность продлить аренду. Если ни один из спонсоров не решит возобновить аренду, менеджер по аренде снимает аренду, и объект может быть собран сборщиком мусора. Менеджер по аренде ведет список аренды, где арендные договоры отсортированы по оставшемуся времени аренды. Аренда с наименьшим оставшимся временем хранится в верхней части списка. Служба удаленного взаимодействия связывает аренду с каждой службой и удаляет службу по истечении времени аренды.
К сожалению, это решение неверно, когда домены приложений используются для целей плагина (сборка плагина не должна загружаться в ваш основной домен приложения).
Вызов GetRealObject() в вашем конструкторе и деструкторе приводит к получению реального типа удаленного объекта, что приводит к попытке загрузки сборки удаленного объекта в текущий домен приложения. Это может вызвать либо исключение (если сборка не может быть загружена), либо нежелательный эффект, связанный с тем, что вы загрузили стороннюю сборку, которую нельзя выгрузить позже.
Лучшее решение может быть, если вы зарегистрируете свои удаленные объекты в основном домене приложения с помощью метода ClientSponsor.Register() (не статично, поэтому вы должны создать экземпляр спонсора клиента). По умолчанию он будет обновлять ваши удаленные прокси через каждые 2 минуты, что достаточно, если у ваших объектов есть время жизни по умолчанию 5 минут.
Тем, кто хочет глубже понять.NET Remoting Framework, я предлагаю статью под названием " Удаленное управление жизненным циклом удаленных объектов.NET с помощью аренды и спонсорства", опубликованную в выпуске журнала MSDN за декабрь 2003 года.
Если вы хотите воссоздать удаленный объект после его сборки, не создавая ISponsor
класс, не давая ему бесконечное время жизни, вы можете вызвать фиктивную функцию удаленного объекта при перехвате 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);
}
}
Я создал класс, который отключился на уничтожение.
public class MarshalByRefObjectPermanent : MarshalByRefObject
{
public override object InitializeLifetimeService()
{
return null;
}
~MarshalByRefObjectPermanent()
{
RemotingServices.Disconnect(this);
}
}
[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
public override object InitializeLifetimeService()
{
return null;
}
Я проверил этот и он работает нормально, конечно, нужно знать, что прокси живет вечно, пока вы не сделаете GC-ing для себя. Но в моем случае, с использованием Plugin-Factory, подключенного к моему основному приложению, нет утечки памяти или чего-то в этом роде. Я просто убедился, что я реализую IDisposable, и он работает нормально (я могу сказать, потому что мои загруженные dll (на фабрике) могут быть перезаписаны, как только фабрика будет расположена правильно)
Редактировать: если ваши события всплывают через домены, добавьте эту строку кода в класс, создавая прокси, в противном случае ваше всплытие также будет выдавать;)
Вы можете попробовать сериализуемый одноэлементный объект ISponsor, реализующий IObjectReference. Реализация GetRealObject (из IObjectReference должна возвращать MySponsor.Instance, когда context.State равен CrossAppDomain, в противном случае возвращать себя. MySponsor.Instance - это самоинициализируемая, синхронизированная (MethodImplOptions.Synchronized), singleton. Реализация Renewal (из ISponsor) должна проверить static MySponsor.IsFlaggedForUnload и возврат TimeSpan.Zero, если он помечен как unload/AppDomain.Current.IsFinalizingForUnload() или в противном случае возвращает LifetimeServices.RenewOnCallTime.
Чтобы присоединить его, просто получите ILease and Register(MySponsor.Instance), который будет преобразован в набор MySponsor.Instance в AppDomain благодаря реализации GetRealObject.
Чтобы прекратить спонсорство, повторно получите ILease и Unregister(MySponsor.Instance), затем установите MySponsor.IsFlaggedForUnload с помощью перекрестного обратного вызова AppDomain (myPluginAppDomain.DoCallback(MySponsor.FlagForUnload)).
Это должно поддерживать ваш объект в другом AppDomain до тех пор, пока не будет отменен вызов отмены регистрации, вызов FlagForUnload или AppDomain.
Я недавно столкнулся с этим исключением также. Прямо сейчас мое решение состоит в том, чтобы просто выгрузить AppDomain, а затем перезагрузить AppDomain после долгого интервала. К счастью, это временное решение работает для моего случая. Хотелось бы, чтобы был более элегантный способ справиться с этим.