Где перехватить неудачное соединение в вызывающем классе WCF?

Я пытаюсь написать класс, который инкапсулирует вызовы WCF (клиент - Silverlight, если это важно). Все работает нормально, но я не уверен, как отловить сбой соединения, как будто сервер не отвечает. Может показаться, что кое-что происходит в результирующем коде ChannelFactory, но я не уверен. Также приветствуется общий обзор кода. :)

Нижняя строка, окружающая создание канала или делегат результата begin или async в try / catch, не перехватывает сбойное соединение. Я бы хотел, чтобы этот улов запускал событие ServiceCallError.

public class ServiceCaller : IDisposable
{
    private IFeedService _channel;

    public ServiceCaller()
    {
        var elements = new List<BindingElement>();
        elements.Add(new BinaryMessageEncodingBindingElement());
        elements.Add(new HttpTransportBindingElement());
        var binding = new CustomBinding(elements);
        var endpointAddress = new EndpointAddress(App.GetRootUrl() + "Feed.svc");
        _channel = new ChannelFactory<IFeedService>(binding, endpointAddress).CreateChannel();
    }

    public void MakeCall(DateTime lastTime, Dictionary<string, string> context)
    {
        AsyncCallback asyncCallBack = delegate(IAsyncResult result)
        {
            var items = ((IFeedService)result.AsyncState).EndGet(result);
            if (ItemsRetrieved != null)
                ItemsRetrieved(this, new ServiceCallerEventArgs(items));
        };
        _channel.BeginGet(lastTime, context, asyncCallBack, _channel);
    }

    public event ItemsRetrievedEventHandler ItemsRetrieved;
    public event ServiceCallErrorHandler ServiceCallError;

    public delegate void ItemsRetrievedEventHandler(object sender, ServiceCallerEventArgs e);

    public delegate void ServiceCallErrorHandler(object sender, ServiceCallErrorEventArgs e);

    public void Dispose()
    {
        _channel.Close();
        _channel.Dispose();
    }
}

Вот трассировка стека для любопытных:

 An AsyncCallback threw an exception.
at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously, Exception exception)
   at System.ServiceModel.Channels.HttpChannelFactory.HttpRequestChannel.HttpChannelAsyncRequest.OnGetResponse(IAsyncResult result)
   at System.Net.Browser.BrowserHttpWebRequest.<>c__DisplayClassd.<InvokeGetResponseCallback>b__b(Object state2)
   at System.Threading._ThreadPoolWaitCallback.WaitCallback_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallbackInternal(_ThreadPoolWaitCallback tpWaitCallBack)
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback(Object state)

Чтобы это произошло, я запускаю приложение в браузере, а затем убиваю процесс веб-сервера из Visual Studio. В тестовой среде я получаю то же самое, отключая сетевое соединение для клиентской системы.

Вот ToString () ПОЛНОГО исключения:

System.Exception: An AsyncCallback threw an exception. ---> System.Exception: An AsyncCallback threw an exception. ---> System.ServiceModel.CommunicationException: The remote server returned an error: NotFound. ---> System.Net.WebException: The remote server returned an error: NotFound. ---> System.Net.WebException: The remote server returned an error: NotFound.
   at System.Net.Browser.BrowserHttpWebRequest.InternalEndGetResponse(IAsyncResult asyncResult)
   at System.Net.Browser.BrowserHttpWebRequest.<>c__DisplayClass5.<EndGetResponse>b__4(Object sendState)
   at System.Net.Browser.AsyncHelper.<>c__DisplayClass2.<BeginOnUI>b__0(Object sendState)
   --- End of inner exception stack trace ---
   at System.Net.Browser.AsyncHelper.BeginOnUI(SendOrPostCallback beginMethod, Object state)
   at System.Net.Browser.BrowserHttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
   at System.ServiceModel.Channels.HttpChannelFactory.HttpRequestChannel.HttpChannelAsyncRequest.CompleteGetResponse(IAsyncResult result)
   --- End of inner exception stack trace ---
   at System.ServiceModel.Channels.Remoting.RealProxy.Invoke(Object[] args)
   at proxy_2.EndGet(IAsyncResult )
   at CoasterBuzz.Feed.Client.ServiceCaller.<MakeCall>b__0(IAsyncResult result)
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
   --- End of inner exception stack trace ---
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously, Exception exception)
   at System.ServiceModel.Channels.ServiceChannel.SendAsyncResult.CallComplete(Boolean completedSynchronously, Exception exception)
   at System.ServiceModel.Channels.ServiceChannel.SendAsyncResult.FinishSend(IAsyncResult result, Boolean completedSynchronously)
   at System.ServiceModel.Channels.ServiceChannel.SendAsyncResult.SendCallback(IAsyncResult result)
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
   --- End of inner exception stack trace ---
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously, Exception exception)
   at System.ServiceModel.Channels.HttpChannelFactory.HttpRequestChannel.HttpChannelAsyncRequest.OnGetResponse(IAsyncResult result)
   at System.Net.Browser.BrowserHttpWebRequest.<>c__DisplayClassd.<InvokeGetResponseCallback>b__b(Object state2)
   at System.Threading._ThreadPoolWaitCallback.WaitCallback_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallbackInternal(_ThreadPoolWaitCallback tpWaitCallBack)
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback(Object state)

Кстати, единственный способ уловить это вообще - использовать событие UnhandledException уровня приложения в клиенте SL.


person Jeff Putz    schedule 30.07.2009    source источник
comment
Есть ли другие внутренние исключения? Полный вывод exception.ToString () гарантирует, что больше ничего нет. Причина, по которой я спрашиваю, заключается в том, что он говорит, что AsyncCallback вызвал исключение, но какое исключение он вызвал? Он делает это только для фатальных исключений и будет включать исходное исключение (я все еще не вижу его здесь).   -  person Anderson Imes    schedule 05.08.2009
comment
Чтобы быть ясным, я разместил этот комментарий после того, как вы опубликовали свое внутреннее исключение ... этого все еще недостаточно.   -  person Anderson Imes    schedule 05.08.2009


Ответы (7)


Джефф, из вашего вопроса я не совсем уверен, как вы имитируете сбой соединения. Я предполагаю, что «как будто сервер не отвечает» означает, что вы ищете какую-то ошибку тайм-аута. Вероятно, вы имитируете неотзывчивый сервер, заставляя медленный ответ от вашего вызова службы с помощью Thread.Sleep () на стороне сервера. Правильно? Достаточно близко?

У меня был похожий проект, поэтому я протестировал его и смог успешно отловить тайм-аут и другие исключения. Я думаю, что, возможно, вы не видели этих исключений, потому что ваша настраиваемая привязка имела очень долгий тайм-аут по умолчанию, и вы, возможно, не ждали достаточно долго.

Чтобы пояснить, тайм-ауты WCF контролируются конфигурацией привязки. Глядя на свой код, вы создаете свою собственную привязку следующим образом:

var elements = new List<BindingElement>();
elements.Add(new BinaryMessageEncodingBindingElement());
elements.Add(new HttpTransportBindingElement());
var binding = new CustomBinding(elements);

Если вы установите там точку останова и проверите созданную вами настраиваемую привязку, вы увидите, что у нее есть одна минута SendTimeout по умолчанию. Я подозреваю, что вы либо не ждали достаточно долго, либо недостаточно долго устанавливали симулируемый тайм-аут для своей службы, поэтому вы не смогли поймать исключение тайм-аута.

Вот некоторые изменения кода для демонстрации. Во-первых, чтобы установить тайм-аут привязки:

var binding = new CustomBinding(elements);
//Set the timeout to something reasonable for your service
//This will fail very quickly
binding.SendTimeout = TimeSpan.FromSeconds(1);

Наконец, чтобы перехватить исключения и вызвать правильные события, вы можете обновить делегат AsyncCallback следующим образом. Исключение возникает при вызове EndGet ().

AsyncCallback asyncCallBack = delegate(IAsyncResult result)
{
    IFeedService service = ((IFeedService)result.AsyncState);
    try
    {
        var items = service.EndGet(result);
        ItemsRetrieved(this, EventArgs.Empty);
    }
    catch (System.TimeoutException ex)
    {
        //Handles timeout
        ServiceCallError(this, EventArgs.Empty);
    }
    catch (System.ServiceModel.CommunicationException ex)
    {
        //Handles a number of failures:
        //  Lack of cross-domain policy on target site
        //  Exception thrown by a service
        ServiceCallError(this, EventArgs.Empty);
    }
    catch (System.Exception ex)
    {
        //Handles any other errors here
        ServiceCallError(this, EventArgs.Empty);
    }
};

Что касается общего обзора кода, я бы рекомендовал вам избегать жесткого кодирования конфигурации привязки. Вместо этого вы можете объявить свою конфигурацию конечной точки (включая разумные тайм-ауты) в файле ServiceReferences.ClientConfig и создать новую фабрику каналов с таким именем конечной точки:

new ChannelFactory<IFeedService>("feedServiceEndpoint");

Это должно со временем сделать приложение более удобным в обслуживании.

Надеюсь, это поможет.

Джерри

Обновлено:

Джефф,

Пожалуйста, взгляните на этот код и комментарии внутри, чтобы получить более подробное объяснение того, что здесь происходит. Метод MakeCall () создает функцию (делегат), которая передается методу BeginGet (). Эта функция позже выполняется, когда служба отвечает.

Внесите предложенные изменения и установите точки останова в строках, начинающихся с «AsyncCallback asyncCallback» и «var items». Вы увидите, что первый проход отладчика просто объявляет код (функцию / делегат) для выполнения, когда служба отвечает, а второй проход - это фактическая обработка этого ответа, начиная с внутренней части объявления вашего делегата. Это означает, что внешний try / catch не будет в области видимости, когда будет обработан ответ на вызов службы.

public void MakeCall(DateTime lastTime, Dictionary<string, string> context)
{
    try
    {
        AsyncCallback asyncCallBack = delegate(IAsyncResult result)
        {
            try
            {
                var items = ((IFeedService)result.AsyncState).EndGet(result);
                if (ItemsRetrieved != null)
                    ItemsRetrieved(this, new ServiceCallerEventArgs(items));
            }
            catch (Exception ex)
            { 
                //This will catch errors from the service call
            }
        };
        _channel.BeginGet(lastTime, context, asyncCallBack, _channel);
    }
    catch(Exception ex)
    {
        //This will not catch an error coming back from the service.  It will 
        //catch only errors in the act of calling the service asynchronously.

        //The communication with the service and response is handled on a different
        //thread, so this try/catch will be out of scope when that executes.

        //So, at this point in the execution, you have declared a delegate 
        //(actually an anonymous delegate the compiler will turn into a hidden class) 
        //which describes some code that will be executed when the service responds.

        //You can see this in action by setting a breakpoint just inside the delegate
        //at the line starting with "var items".  It will not be hit until the service
        // responds (or doesn't respond in a timeout situation).
    }
}

Джерри

person Jerry Bullard    schedule 02.08.2009
comment
Нет, как я уже упоминал, я попытался отловить ошибку во всех точках касания, создании канала, начале и завершении вызова. Имеет смысл, что захват не сработает, потому что начало и конец происходят в их собственном потоке, верно? Именно в этом коде, который для меня в рамках фреймворка выглядит ужасно черным ящиком, это происходит. - person Jeff Putz; 02.08.2009
comment
Трассировка стека указывает, что исключение было вызвано асинхронным обратным вызовом. Вероятно, это происходит внутри вашего объявленного делегата при вызове метода EndGet (). Пожалуйста, попробуйте еще раз разместить блок try / catch вокруг содержимого вашего AsyncCallback (внутри, а не снаружи, где он объявлен). Возможно, вы уже пробовали это, но, пожалуйста, опубликуйте этот код для подтверждения. Если вы не получаете исключения при вызове EndGet (), установите точку останова в строке, начинающейся с var items. Затем добавьте эти часы: ((System.ServiceModel.AsyncResult) (result)). Exception Это не должно быть нулевым. - person Jerry Bullard; 04.08.2009
comment
Пробовал перед этим. Как я сказал изначально, я пытался уловить это в каждой точке этого класса, и ни одна из них не улавливала исключение. Единственное место, где я смог его получить, - это событие UnhandledException на уровне приложения SL. - person Jeff Putz; 05.08.2009
comment
Опубликуйте код с дополнительным предложением try / catch и установите точку останова в этой строке кода: var items = ((IFeedService) result.AsyncState) .EndGet (result); Затем перед выполнением этой строки сообщите нам значение this: ((System.ServiceModel.AsyncResult) (result)). Exception - person Jerry Bullard; 05.08.2009

Раньше я сталкивался с подобной проблемой. Основная проблема заключается в том, как IDisposable реализован на экземплярах вашего прокси / канала. То, как я решил эту проблему, показано в приведенном ниже коде, где IDirector - мой контракт на обслуживание:

public class ProxyWrapper : IDisposable
{
    private IDirector proxy;
    private ChannelFactory<IDirector> factory;
    int callCount = 0;

    public ProxyWrapper()
    {
        factory = new ChannelFactory<IDirector>();

        proxy = factory.CreateChannel();
    }

    public IDirector Proxy
    {
        get
        {
            if (callCount > 0)
                throw new InvalidOperationException("The proxy can only be accessed once for every ProxyWrapper instance. You must create a new ProxyWrapper instance for each service request.");
            // only allow the proxy/channel to be used for one call.

            callCount++;
            return proxy;
        }
    }

    public void Dispose()
    {
        IClientChannel channel = (IClientChannel)proxy;

        try
        {
            if (channel.State != CommunicationState.Faulted)
            {
                channel.Close();
            }
            else
            {
                channel.Abort();
            }
        }
        catch (CommunicationException)
        {
            channel.Abort();
        }
        catch (TimeoutException)
        {
            channel.Abort();
        }
        catch (Exception)
        {
            channel.Abort();
            throw;
        }
        finally
        {
            channel = null;
            proxy = null;
        }
    }
}

Способ использования вышеуказанного класса следующий:

    public static void Login(string userName, string password)
    {
        using (ProxyWrapper wrapper = new ProxyWrapper())
        {
            currentSession = wrapper.Proxy.Login(userName, password);
        }
    }

Поскольку класс ProxyWrapper реализует IDisposable, если мы используем экземпляр класса ProxyWrapper внутри блока using, метод Dispose() гарантированно будет вызван, даже если возникнет исключение. Код в методе Dispose() будет обрабатывать все случаи и состояния прокси / канала. Затем вы можете добавить в этот метод код делегата обработки ошибок / ведения журнала / событий.

Прочтите следующую запись в блоге для получения дополнительной информации и более общей версии приведенного выше кода: http://bloggingabout.net/blogs/erwyn/archive/2006/12/09/WCF-Service-Proxy-Helper.aspx

person Saajid Ismail    schedule 07.08.2009

Чтобы отловить ошибки связи с сервером, я бы предложил поместить вызов .CreateChannel() в try ... catch и обработать там такие вещи, как CommunicationException и TimeoutExceptions.

Кроме того, один из основных советов: создание ChannelFactory - довольно дорогостоящая операция, поэтому вы можете сделать это отдельно и где-нибудь сохранить эту ссылку на объект ChannelFactory.

Если вам нужно создать и, возможно, повторно создать канал из ChannelFactory, вы можете затем использовать этот сохраненный / кэшированный экземпляр ChannelFactory снова и снова, не создавая его все время.

Но в остальном, я думаю, у вас здесь все хорошо!

Марк

person marc_s    schedule 30.07.2009
comment
Как я уже говорил, это не работает. Когда я отключаю сервер, клиент отвечает, что AsyncCallback генерирует исключение, а трассировка стека не включает в себя какую-либо часть моего кода. - person Jeff Putz; 30.07.2009
comment
Что, если вы поместите свой код в метод асинхронного обратного вызова (я вижу, вы используете для этого анонимного делегата) в блок try .... catch ?? Обычно при асинхронной операции, если сервер выдал исключение, вы получите это на клиенте при вызове метода .EndXXXXX(). - person marc_s; 31.07.2009
comment
Нет ... все равно не поймаешь исключение. Странно, а? - person Jeff Putz; 31.07.2009

Ваш экземпляр ChannelFactory будет в состоянии «Прервано», когда произойдет этот тайм-аут. Вы обнаружите, что исключение генерируется не тогда, когда исключение происходит в вызове, а когда вы вызываете _channel.Dispose ().

Вам нужно как-то защититься от этого исключения. Самый простой способ - это попытаться / уловить содержимое вашего метода удаления, но было бы лучше, если бы вы проверили состояние экземпляра _channel, прежде чем пытаться закрыть / удалить.

Вот почему вы все еще получаете исключение - здесь оно не обрабатывается.

Обновление:

Судя по трассировке стека, похоже, что фреймворк пытается выполнить ваш делегат обратного вызова и получает исключение. Вы ответили в другом месте, что попытка / перехват содержимого вашего делегата не решает проблему ... как насчет внутренних исключений? Каков полный вывод TheException.ToString ()?

Я пробежался через .NET Framework, и ему действительно разрешено возвращаться к пулу потоков (по крайней мере, в трассировке стека, которая у вас есть), что приведет к необработанному исключению и к смерти вашего потока. Если что-то еще хуже, вы всегда можете взять на себя потоки ... развернуть свой собственный поток и синхронно вызвать службу внутри, а не использовать встроенный асинхронный шаблон (если это не тип связи WCF Duplex, который я не делаю) не думаю, что это так).

Я все еще думаю, что полная информация об исключениях (если есть что-то еще) может быть полезна.

person Anderson Imes    schedule 03.08.2009
comment
Нет ... без радости. Как я уже сказал, в трассировке стека ничего не имеет отношения к моему коду. - person Jeff Putz; 04.08.2009
comment
Он ничего не покажет из вашей трассировки стека. Попробуйте применить try / catch к вашим _channel.Dispose () и _channel.Close () и посмотрите, что произойдет. - person Anderson Imes; 04.08.2009
comment
Как я уже сказал (без радости), добавление здесь try / catch не работает. Я отредактировал оригинал выше, чтобы показать след. - person Jeff Putz; 04.08.2009
comment
Неважно, я вижу, что вы это уже пробовали ... Я обновил свой ответ запросом полного вывода TheException.ToString () (также показывает все внутренние исключения). - person Anderson Imes; 04.08.2009
comment
Внутреннее исключение добавлено в основную часть этого сообщения. - person Jeff Putz; 05.08.2009

Джефф, покажите нам свой код, когда вы попытаетесь поместить блок try / catch вокруг вызова EndGet. Трудно поверить, что это не исключение, и видение этого поможет мне поверить в это.

Кроме того, в рамках этого эксперимента избавьтесь от обработчика событий UnhandledException. Я думаю, вы поймаете это исключение, как только BrowserHttpRequest поймет, что есть 404. Я думаю, что без события UnhandledException попытка / уловка вокруг EndGet перехватит исключение, если оно есть. Я думаю, вы улавливаете промежуточное состояние процесса обработки исключений.

Я также хотел бы, чтобы вы поместили System.Debugger.Break или что-то в этом роде сразу после вызова EndGet.

person John Saunders    schedule 05.08.2009

Прав ли я, думая, что вы создали свой ServiceCaller класс так, чтобы его можно было вызывать в using блоке?

Если да, то в этом проблема. Классы каналов WCF были спроектированы таким образом, чтобы было чрезвычайно сложно написать полностью общий код утилизации, который учитывает все возможные состояния ошибок. Если бы это было просто, то сами классы каналов можно было бы безопасно удалить, и вам не понадобился бы класс-оболочка.

Единственный безопасный способ, который я нашел на сегодняшний день, - это инкапсулировать не логику удаления, а всю последовательность вызовов, таким образом:

public static void Invoke<TContract>(ChannelFactory<TContract> factory, Action<TContract> action) where TContract : class {

    var proxy = (IClientChannel) factory.CreateChannel();
    bool success = false;
    try {
        action((TContract) proxy);
        proxy.Close();
        success = true;
    } finally {
        if(!success) {
            proxy.Abort();
        }
    }
}

Я знаю, что это синхронный вызов, но принцип тот же. Вместо того, чтобы пытаться определить, следует ли вам вызывать Close или Abort, вы определяете это заранее, основываясь на том, как далеко вам удалось пройти через вызов, прежде чем он упал.

Обратите внимание: в этой специальной обработке требуется только реальный канал. Вы можете избавиться от фабрики как хотите, AFAIK.

person Christian Hayter    schedule 06.08.2009
comment
Совершать синхронные вызовы в среде пользовательского интерфейса рабочего стола - не лучшая идея. - person Jeff Putz; 06.08.2009
comment
Это решение зависит от вас, я только пытался проиллюстрировать, как вы должны отслеживать точный метод удаления канала. Для вашего асинхронного класса вы можете добавить поле состояния уровня класса, содержащее такие состояния, как Initialized, Opened, Closed и т. Д. Затем ваш метод Dispose будет вызывать либо Abort, либо Close, либо вообще ничего в зависимости от значения вашего поля состояния. - person Christian Hayter; 06.08.2009
comment
Я согласен с вашим утверждением о том, что синхронные вызовы в среде пользовательского интерфейса рабочего стола - плохая идея, но вам, конечно, не нужно использовать шаблон async, предоставляемый SVCUTIL. Вы также можете легко выполнить синхронный вызов из фонового потока (используя BackgroundWorker или аналогичный подход). Это позволит избежать связывания потока пользовательского интерфейса и по-прежнему даст вам то, что вы хотите - возможность изящно перехватывать эти исключения. - person Anderson Imes; 07.08.2009

Я думаю, что проблема в том, как вы спроектировали вызывающего абонента.

Канал открывается при создании вызывающей стороны службы. Это означает, что время ожидания канала истекает, и в вашем коде нет ничего, что можно было бы восстановить по тайм-ауту.

Я бы перенес создание и закрытие канала в метод make call.

Вы можете попробовать захватить начало и содержимое обратного вызова.

person Shiraz Bhaiji    schedule 07.08.2009