Где перехватить неудачное соединение на вызывающем классе WCF? - PullRequest
5 голосов
/ 30 июля 2009

Я пытаюсь написать класс, который инкапсулирует вызовы 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 () FULL-исключения:

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.

Ответы [ 7 ]

8 голосов
/ 02 августа 2009

Джефф, я не совсем уверен из твоего вопроса, как ты имитируешь сбой соединения. Я предполагаю, что «как будто сервер не отвечает» означает, что вы ищете какую-то ошибку тайм-аута. Вы, вероятно, имитируете неотвечающий сервер, форсируя медленный ответ на ваш вызов службы с помощью 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");

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

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

Jerry

Обновлен:

Джеф,

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

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

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).
    }
}

Jerry

4 голосов
/ 07 августа 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

0 голосов
/ 07 августа 2009

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

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

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

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

0 голосов
/ 06 августа 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.

0 голосов
/ 05 августа 2009

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

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

Мне бы также хотелось, чтобы вы поместили System.Debugger.Break или что-то еще сразу после вызова EndGet.

0 голосов
/ 03 августа 2009

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

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

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

Обновление:

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

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

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

0 голосов
/ 30 июля 2009

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

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

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

Но, кроме этого, я думаю, у вас все хорошо!

Марк

...