2009-07-30 9 views
5

Próbuję napisać klasy, która hermetyzuje wywołania WCF (klient jest Silverlight, jeśli ma to znaczenie). Wszystko działa płynnie, ale nie jestem pewien, jak złapać połączenie, tak jakby serwer nie reagował. Wydaje się, że trochę pracy dzieje się gdzieś w wynikowym kodzie z ChannelFactory, ale nie jestem pewien. Ogólny przegląd kodu jest również mile widziany. :)Gdzie pułapkować nieudane połączenie w klasie wywołania WCF?

Dolna linia, otaczająca utworzenie kanału, lub początek lub asynchroniczny wynik delegata w próbie/catch nie przechwytuje nieudanego połączenia. Chciałbym, aby to catch uruchomiło zdarzenie 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(); 
    } 
} 

Oto ślad stosu dla tych, którzy są ciekawi:

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) 

Aby tak się stało, ja odpalić aplikację w przeglądarce, a potem zabić proces serwera sieci Web z Visual Studio. W środowisku testowym otrzymuję to samo, zabijając połączenie sieciowe dla systemu klienta.

Oto ToString pełnej Wyjątkiem jest():

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) 

Nawiasem mówiąc, jedynym sposobem mogę złapać to w ogóle jest wykorzystanie poziomu aplikacji zdarzenie UnhandledException w kliencie SL.

+0

Jakieś inne wyjątki wewnętrzne? Pełna wydajność wyjątku.ToString() zapewniłaby, że nie ma nic więcej. Powodem, dla którego pytam jest to, że AsyncCallback rzucił wyjątek, ale jaki wyjątek wyrzucił? Robi to tylko w przypadku wyjątków krytycznych i będzie zawierał oryginalny wyjątek (wciąż go tutaj nie widzę). –

+0

Dla jasności, opublikowałem ten komentarz po opublikowaniu wewnętrznego wyjątku ... to nadal nie wygląda wystarczająco. –

+0

OK ... wszystko już jest. –

Odpowiedz

8

Jeff, nie jestem do końca pewien z pytaniem, w jaki sposób są symulowanie awarii połączenia. Zakładam, że "tak jakby serwer nie odpowiadał" oznacza, że ​​szukasz pewnego rodzaju błędu limitu czasu. Prawdopodobnie symulujesz serwer, który nie reaguje, wymuszając powolną odpowiedź ze zgłoszenia serwisowego za pomocą Thread.Sleep() po stronie serwera. Dobrze? Wystarczająco blisko?

Miałem podobny projekt, więc przetestowałem to i udało mi się z powodzeniem przechwycić limit czasu i inne wyjątki. Myślę, że być może nie widziałeś tych wyjątków, ponieważ twoje niestandardowe powiązanie miało bardzo długi domyślny limit czasu i możesz nie czekać wystarczająco długo.

Aby to wyjaśnić, limity czasu WCF są kontrolowane przez konfigurację wiązania. Patrząc na kod, tworzysz własną wiążącą tak niestandardowe:

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

Jeśli ustawisz przerwania tam i sprawdzić zwyczaj wiązania utworzone, widać, że ma jedną minutę SendTimeout domyślnie. Podejrzewam, że nie czekałeś wystarczająco długo lub nie ustawiłeś symulowanego limitu czasu usługi, więc nie udało Ci się złapać wyjątku limitu czasu.

Oto niektóre zmiany w kodzie do wykazania. Po pierwsze, aby ustawić limit czasu na wiązanie:

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

Wreszcie, aby złapać wyjątki i podnieść odpowiednie zdarzenia, można zaktualizować delegata AsyncCallback następująco. Wyjątek jest generowany, gdy wywoływana jest metoda 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); 
    } 
}; 

Jeśli chodzi o ogólny przegląd kodu, to zaleciłbym uniknięcie ciągłego kodowania konfiguracji wiązania. Można zamiast deklarować swoją konfigurację punktu końcowego (włączając w to rozsądne limity czasu) w pliku ServiceReferences.ClientConfig i nowa fabryka w górę kanał o nazwie końcowego takiego:

new ChannelFactory<IFeedService>("feedServiceEndpoint"); 

To powinno sprawić, że aplikacja bardziej linkujących w czasie.

Mam nadzieję, że to pomoże.

Jerry

Aktualizacja:

Jeff

Proszę spojrzeć na ten kod i komentarze w obrębie więcej wyjaśnień co się tutaj dzieje. Metoda MakeCall() tworzy funkcję (delegate), która jest przekazywana do metody BeginGet(). Ta funkcja jest później wykonywana, gdy usługa odpowiada.

Proszę dokonać zmian sugerowanych i ustawić punkty przerwania na linie zaczynające się od „AsyncCallback AsyncCallback” i „pozycje” var. Pierwsze przejście przez debuggera jest po prostu deklaracją kodu (funkcji/delegata) do wykonania, gdy usługa odpowiada, a drugie przejście jest rzeczywistym przetwarzaniem tej odpowiedzi, zaczynając od wewnętrznej strony deklaracji delegata. Oznacza to, że zewnętrzny try/catch nie będzie w zasięgu, gdy przetwarzana jest odpowiedź zgłoszenia serwisowego.

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

+0

Nie, jak już wspomniałem, próbowałem złapać pułapkę błędu we wszystkich punktach kontaktu, utworzeniu kanału, rozpoczęciu połączenia i zakończeniu połączenia. Ma sens, że pułapka nie zadziała, ponieważ początek i koniec ma miejsce w ich własnej nić, prawda? Ten kod, który jest czarnie czarno-biały w moim wyglądzie w ramach, jest tam, gdzie to się dzieje. –

+2

Śledzenie stosu wskazuje, że wyjątek został zgłoszony przez asynchroniczne wywołanie zwrotne. Prawdopodobnie zostanie to zgłoszone w zadeklarowanym uczestniku przy wywołaniu metody EndGet(). Spróbuj ponownie umieścić blok try/catch wokół zawartości AsyncCbackback (wewnątrz, nie na zewnątrz, gdzie jest zadeklarowany). Być może już to wypróbowałeś, ale opublikuj ten kod, aby potwierdzić. Jeśli nie otrzymasz wyjątku w wywołaniu EndGet(), ustaw punkt przerwania w linii zaczynając od "var items". Dodaj ten zegarek: ((System.ServiceModel.AsyncResult) (wynik)). Wyjątek To nie powinno być puste. –

+0

Wypróbowałem to wcześniej. Jak mówiłem pierwotnie, próbowałem uwięzić to w każdym punkcie tej klasy, i żaden z nich nie złapał wyjątku. Jedynym miejscem, w którym udało mi się go zdobyć, jest wydarzenie UnhandledException w aplikacji na poziomie SL. –

0

Aby złapać błędy komunikacyjne z serwerem, sugeruję połączenie z .CreateChannel(), aby spróbować ... złapać i obsłużyć takie rzeczy, jak wyjątki CommunicationException i TimeoutExceptions.

Również jedna ogólna rada: tworzenie ChannelFactory jest dość kosztowną operacją, więc możesz chcieć to zrobić osobno i przechowywać gdzieś odniesienie do obiektu ChannelFactory.

Jeśli chcesz utworzyć i ewentualnie ponownie utworzyć kanał z ChannelFactory, możesz użyć tej przechowywanej/buforowanej instancji ChannelFactory w kółko bez konieczności ciągłego tworzenia tego.

Ale poza tym, myślę, że tutaj wszystko w porządku!

Marc

+0

Jak już wspomniałem, to nie działa. Kiedy odetnę serwer, klient odpowie "AsyncCallback rzucił wyjątek", a ślad stosu nie zawiera żadnej części mojego kodu. –

+0

Co jeśli położysz kod w asynchronicznej metodzie wywołania zwrotnego (widzę, że używasz anonimowego delegata do tego) do próby .... catch bloku? Zwykle w przypadku operacji asynchronicznej, jeśli serwer zgłosił wyjątek, otrzymasz to u klienta po wywołaniu metody '.EndXXXXX()'. –

+0

Nie ... nadal nie złapie wyjątku. Dziwne, co? –

0

Twój ChannelFactory instancja będzie w „przerwana” stan po wystąpieniu tego limitu czasu. Przekonasz się, że wyjątek jest zgłaszany nie wtedy, gdy wyjątek ma miejsce w wywołaniu, ale kiedy wywołujemy _channel.Dispose().

W jakiś sposób musisz uchronić się przed tym wyjątkiem. Najprostszym sposobem byłoby umieszczenie try/catch wokół zawartości metody dispose, ale byłoby lepiej, gdybyś sprawdził stan instancji _channel przed próbą zamknięcia/utylizacji.

To jest powód, dla którego nadal otrzymujesz wyjątek - nie jest tu obsługiwany.

Aktualizacja:

Na podstawie śladu stosu, wygląda na to ramy stara się wykonywać swoją delegata wywołania zwrotnego i jest coraz tam wyjątek. Odpowiedziałeś gdzie indziej, że próba/złapanie zawartości twojego delegata nie rozwiązuje problemu ... a co z wewnętrznymi wyjątkami? Jaka jest pełna wydajność TheException.ToString()?

Przechwyciłem system .NET Framework i rzeczywiście można przejść całą drogę z powrotem do wątku (przynajmniej w stosie, który tu masz), co spowodowałoby nieobsłużony wyjątek i wątek, który umarł.Jeśli gorsze jest gorzej, zawsze możesz przejąć wątek ... zakręcić własny wątek i wewnątrz wywoływać usługę synchronicznie, zamiast korzystać z wbudowanego wzorca asynchronicznego (chyba że jest to komunikacja typu Duplex WCF, której nie używam). tak sobie myślę).

Nadal uważam, że pełne informacje o wyjątku (jeśli jest coś jeszcze) mogą być przydatne.

+0

Nie ... nie radość. Tak jak powiedziałem, nic w śladzie stosu w ogóle nie dotyczy mojego kodu. –

+0

Nie pokazywałoby niczego ze śladu stosu. Spróbuj spróbować/złapać wokół _channel.Dispose() i _channel.Close() i zobacz, co się stanie. –

+0

Co więcej, opublikuj ślad stosu, co? Mógł mieć dobrą wskazówkę. –

0

Jeff, prosimy o pokazanie nam swojego kodu przy próbie umieszczenia bloku prób/catch wokół połączenia EndGet. Trudno uwierzyć, że nie ma wyjątku, a zobaczenie go pomoże mi uwierzyć.

Ponadto, w ramach tego eksperymentu, pozbyć się obsługi zdarzeń UnhandledException. Myślę, że łapiecie ten wyjątek, gdy tylko BrowserHttpRequest zorientuje się, że istnieje 404. Myślę, że bez zdarzenia UnhandledException, próba/catch wokół EndGet złapie wyjątek, jeśli taki będzie. Myślę, że wybierasz stan pośredni procesu obsługi wyjątków.

Chciałbym również zobaczyć, jak umieszczasz System.Debugger.Break lub coś bezpośrednio po wywołaniu EndGet.

0

Mam rację myśląc, że zaprojektowałeś swoją klasę ServiceCaller, aby można było wywołać ją w bloku using?

Jeśli tak, to jest problem. Klasy kanału WCF zostały zaprojektowane w taki sposób, aby niezwykle trudno było napisać całkowicie ogólny kod dozowania, który spełnia wszystkie możliwe warunki błędu. Gdyby było to proste, to same klasy kanałów byłyby bezpieczne i nie potrzebowalibyśmy klasy opakowania.

Jedynym bezpiecznym sposobem znalazłem do tej pory to nie hermetyzacji logiki zbyć, ale cała sekwencja połączeń, a więc:

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

wiem, że to synchroniczne wywołanie, ale zasada jest taka sama. Zamiast próbować wydedukować, czy powinieneś zadzwonić pod numer Close lub Abort, określasz to z góry na podstawie tego, jak daleko udało ci się uzyskać połączenie, zanim udało ci się go pokonać.

Uwaga - tylko właściwy kanał wymaga tego specjalnego traktowania. Możesz pozbyć się fabryki w dowolny sposób, w jaki lubisz AFAIK.

+0

Wywoływanie wywołań synchronicznych w środowisku interfejsu użytkownika pulpitu nie jest dobrym pomysłem. –

+0

Ta decyzja należy do ciebie, próbowałem tylko zilustrować, jak powinieneś śledzić dokładną metodę unieszkodliwienia kanału. Dla twojej klasy asynch możesz dodać pole stanu na poziomie klasy zawierające takie stany, jak Zainicjowane, Otwarte, Zamknięte, itp. Twoja metoda Zrzutu wywoła wtedy albo Przerwij, Zamknij albo nic w ogóle, w zależności od wartości twojego pola stanu. –

+1

Zgadzam się ze stwierdzeniem dotyczącym wywołań synchronicznych w środowisku pulpitu interfejsu użytkownika, jest to zły pomysł, ale z pewnością nie musisz używać wzorca asynchronicznego dostarczonego przez SVCUTIL. Równie łatwo można wykonać synchroniczne wywołanie z wątku tła (przy użyciu programu BackgroundWorker lub podobnego podejścia). Pozwoli to uniknąć związania wątku UI i nadal dostarczy ci to, czego chcesz - umiejętnie złapać te wyjątki. –

4

Zetknąłem się z podobnym problemem wcześniej. Głównym problemem jest sposób implementacji IDisposable w instancjach twojego proxy/kanału. Sposób I został rozwiązany to pokazano w poniższym kodzie, gdzie IDirector jest moja umowa o świadczenie usług:

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

droga do korzystania z wyżej klasy jest następujący:

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

Ponieważ klasa ProxyWrapper implementuje IDisposable, jeśli użyjemy instancji klasy ProxyWrapper wewnątrz bloku using, metoda Dispose() ma być wywołana, nawet jeśli zostanie zgłoszony wyjątek. Kod w metodzie Dispose() obsłuży wszystkie przypadki i stany proxy/kanału. Następnie możesz dodać swój kod obsługi błędów/rejestrowania/przekazywania zdarzeń w tej metodzie.

Przeczytaj poniższy wpis w blogu, aby uzyskać więcej informacji, a dla bardziej ogólny wersji kodu powyżej: http://bloggingabout.net/blogs/erwyn/archive/2006/12/09/WCF-Service-Proxy-Helper.aspx

0

myślę, że problem jest w sposób, w jaki zostały zaprojektowane rozmówcę serwisowym.

Kanał jest otwierany po utworzeniu osoby dzwoniącej. Oznacza to, że kanał może przekroczyć limit czasu i nie ma nic w kodzie, który mógłby odzyskać dane z timeount.

Chciałbym przenieść tworzenie i zamykanie kanału do metody wywołania make.

Możesz spróbować złapać wokół początku i dookoła zawartości wywołania zwrotnego.