2016-08-12 53 views
5

Chciałbym uruchomić długo działające zadanie (np. 4-5 minut) w ApiControllerze w środowisku hostowanym samodzielnie OWIN. Jednak chciałbym wysłać odpowiedź z powrotem po uruchomieniu tego zadania (jak tylko uruchomiłem zadanie długotrwałe) bez czekania, aż to się skończy. To długo działające zadanie nie ma nic wspólnego z HTTP i sekwencyjnie uruchamia niektóre metody, które mogą zająć bardzo dużo czasu.Długie uruchamianie zadania w ApiControllerze (przy użyciu WebAPI, selfhosted OWIN)

Przeczytałem na blogu this i postanowiłem spróbować pod numerem QueueBackgroundWorkItem. Jednak nie jestem pewien, czy możliwe jest użycie tej metody w środowisku hostowanym samodzielnie (aplikacja konsolowa) lub konieczne do korzystania z tej metody. W samodzielnej aplikacji konsolowej myślę, że sama aplikacja zarządza żądaniami i wszystkie żądania działają w tym samym AppDomain (aplikacje domyślnie AppDomain, nie tworzymy żadnych nowych appDomain), więc mogę po prostu uruchomić długo działające zadanie moda na ogień i zapomnienie, bez robienia czegoś wyjątkowego?

W każdym razie, kiedy używam QueueBackgroundWorkItem, zawsze pojawia się błąd:

<Error> 
<Message>An error has occurred.</Message> 
<ExceptionMessage> 
Operation is not valid due to the current state of the object. 
</ExceptionMessage> 
<ExceptionType>System.InvalidOperationException</ExceptionType> 
<StackTrace> 
at System.Web.Hosting.HostingEnvironment.QueueBackgroundWorkItem(Func`2 workItem) at BenchMarkService.SmokeTestController.IsItWorking() in C:\Devel\Code\Projects\BenchMarkService\BenchMarkService\SmokeTestController.cs:line 18 at lambda_method(Closure , Object , Object[]) at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClass10.<GetExecutor>b__9(Object instance, Object[] methodParameters) at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.Execute(Object instance, Object[] arguments) at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ExecuteAsync(HttpControllerContext controllerContext, IDictionary`2 arguments, CancellationToken cancellationToken) --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Web.Http.Controllers.ApiControllerActionInvoker.<InvokeActionAsyncCore>d__0.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Web.Http.Controllers.ActionFilterResult.<ExecuteAsync>d__2.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext() 
</StackTrace> 
</Error> 

Również znalazłem this pytanie na SO i szczerze mówiąc wydaje się trochę mylące dla mnie, jako jedynej drogi do osiągnięcia tego to jest IRegisteredObject czy nie?

Po prostu próbuję uruchomić długo działające zadanie w aplikacji hostowanej samodzielnie i doceniam wszelkie pomysły na ten temat. Niemal wszystkie zasoby, pytania, które znalazłem, są oparte na asp .net i nie jestem pewien, od czego zacząć.

+0

Czy to nie jest opcja, aby umieścić w kolejce zadanie (w bazie danych) i pozwolić, aby jakaś usługa go obsłużyła? –

+0

Hangfire (https://www.hangfire.io/) robi coś podobnego, o ile wiem.Uważam jednak, że to trochę inaczej niż pytanie. – Deniz

Odpowiedz

2

self-hosted aplikacje OWIN są zazwyczaj regularne aplikacje konsolowe, zatem można wydzielić trochę długo uruchomione zadanie w taki sam sposób, jak to zrobić w aplikacji konsoli, np

  • Task.Run
  • ThreadPool.QueueUserWorkItem
  • new Thread(...).Start()

W IIS gospodarzem aplikacji ASP.NET nie jest wskazane stosowanie takich metod ponieważ pule aplikacji są często poddawane recyklingowi, dlatego twoja aplikacja jest często zamykana i ponownie uruchamiana. W takim przypadku zadanie w tle zostanie przerwane. Aby zapobiec (lub odroczyć), API, takie jak QueueBackgroundWorkItem zostały wprowadzone.

Jednak ponieważ jesteś hostowany samodzielnie i nie działa w IIS, możesz po prostu użyć wymienionych powyżej interfejsów API.

2

Musisz podać mechanizm dodawania zadania do czegoś poza kontrolerem.

Zazwyczaj w aplikacjach używam owin dla faktycznego hosta IIS, więc doszedłem do wniosku, że mogę używać "procesów hostowanych", ale w aplikacji konsolowej będzie to znacznie prostsze.

Najbardziej oczywistym podejściem, które przychodzi do głowy to przekazać w jakimś globalnym obiektu/Singleton, że ramy DI wie o mogą być pewni, jest zawsze ten sam obiekt jako „kontener dla zadania”

public class FooController : ApiController 
{ 
    ITaskRunner runner; 

    public FooController(ITaskRunner runner) { this.runner = runner; } 

    Public IActionResult DoStuff() { 
     runner.AddTask(() => { Stuff(); }); 
     return Ok(); 
    } 
} 

Osoba wykonująca zadania może wykonać zadanie, ponieważ jest niewiele więcej niż zwykłym opakowaniem wokół zadania.Run(), jeśli chcesz, ale posiadanie tego haka i przekazanie go oznacza, że ​​po tym, jak żądanie zostanie "obsłużone", a kontroler zostanie wyczyszczony, zadanie jest nadal uważane za "w zakresie", więc aplikacja może kontynuować przetwarzanie zamiast próbując to wyczyścić.

2

Chciałem skomentować odpowiedź na wojnę, ale odpowiedź na moje własne pytanie wydaje się być lepsza dla długich wyjaśnień. To może nie być pełna odpowiedź, przepraszam.

Rozwiązanie wojny to możliwy sposób na wdrożenie tej funkcji i zrobiłem coś podobnego do tego. Zasadniczo utwórz składnicę zadań, a po każdym uruchomieniu lub zakończeniu nowego zadania dodaj/usuń to zadanie do magazynu zadań. Chciałbym jednak wspomnieć o pewnych kwestiach, ponieważ uważam, że nie jest to tak proste jak to.

1) Zgadzam się z wykorzystaniem schematu wtrysku zależności w celu wdrożenia wzorca singleton. W tym celu skorzystałem z metody Autofac "SingleInstance()". Jednak, jak widać w wielu odpowiedziach i zasobach w Internecie, wzorzec singletonowy nie jest popularnym wzorcem, chociaż czasami wydaje się nieodzowny.

2) Twoje repozytorium powinno być wątkowo bezpieczne, więc dodawanie/usuwanie zadań z tego magazynu zadań powinno być bezpieczne dla wątków. Możesz używać współbieżnej struktury danych, takiej jak "ConcurrentDictionary" zamiast blokowania (co jest kosztowną operacją pod względem czasu procesora).

3) Możesz rozważyć użycie Tonu Anulowania dla asynchronizacji. operacje. Jest wysoce prawdopodobne, że aplikacja może zostać zamknięta przez użytkownika lub wyjątek, który może być obsługiwany, może wystąpić w czasie wykonywania. Długotrwałe zadania mogą być wąskim gardłem dla płynnego zamykania/restartowania operacji, szczególnie jeśli masz wrażliwe dane do stracenia i używasz innych zasobów, takich jak pliki, które mogą być w tym przypadku prawidłowo zamknięte. Chociaż asynchroniczny. Metody nie mogą być anulowane natychmiast po każdej próbie anulowania zadania za pomocą tokena, nadal warto spróbować funkcji AnulowanieZwolnień lub podobnych mechanizmów, aby w razie potrzeby zatrzymać/anulować długo trwające zadania.

4) Samo dodanie nowego zadania do magazynu danych nie wystarcza, a także usunięcie zadania, które zakończyło się pomyślnie, czy nie. Próbowałem wypalić zdarzenie, które sygnalizuje zakończenie zadania, a następnie usunąłem to zadanie z magazynu zadań. Jednak nie jestem z tego zadowolony, ponieważ wystrzelenie zdarzenia, a następnie powiązanie metody z tym wydarzeniem w celu otrzymania zadania jest mniej więcej jak kontynuacja licencji TPL. To jak odkrywanie koła, a także zdarzenia mogą powodować podstępne problemy w środowisku wielowątkowym.

Mam nadzieję, że to pomoże. Preferuję dzielenie się niektórymi doświadczeniami zamiast publikowania kodu, ponieważ nie wierzę, że mam najlepsze rozwiązanie tego problemu. Odpowiedź na wojnę daje zgrubny pomysł, ale zanim to zrobisz w systemie gotowym do produkcji, musisz wziąć pod uwagę wiele problemów.

Edytuj: W przypadku długotrwałych zadań możesz rzucić okiem na wątek this. Dobrą praktyką jest zarządzanie pulą wątków.

+0

Yeh moje myśli dokładnie ... Starałem się nie bagatelizować mojej odpowiedzi zbyt dużą ilością informacji w tle, które mogą lub nie mogą przyjąć założenia dotyczące twojej implementacji. Aby uzyskać kompletne rozwiązanie, należy unikać robienia czegoś tak prostego na rzecz spełnienia standardów wzorców/kodowania w repozytorium kodu. Jeśli chodzi o wątki, powinieneś zająć się warstwą danych/logiką biznesową, więc na tym poziomie nie martwiłbym się wyrzucaniem wątków/zadań. Moje 2p ... cieszę się, że to rozwiązałeś :) – War

3

To jest prawie to, do czego stworzono Hangfire (https://www.hangfire.io/).

Brzmi jak praca z ogniem i zapomnieniem.

var jobId = BackgroundJob.Enqueue(
    () => Console.WriteLine("Fire-and-forget!"));