2016-08-12 4 views
5

Ich würde gerne eine lange laufende Aufgabe (sagen wir 4-5 Minuten) in einem ApiController innerhalb einer selbst gehosteten OWIN-Umgebung ausführen. Ich möchte jedoch eine Antwort zurücksenden, nachdem ich diese Aufgabe gestartet habe (sobald ich die lange laufende Aufgabe gestartet habe), ohne darauf zu warten, dass sie beendet wird. Diese lange laufende Aufgabe hat nichts mit dem HTTP zu tun und führt sequentiell einige Methoden aus, die sehr lange dauern können.Lange laufende Aufgabe in ApiController (mit WebAPI, selbst gehosteten OWIN)

Ich sehe this Blog-Post und beschlossen, einen Versuch zu QueueBackgroundWorkItem geben. Ich bin mir jedoch nicht sicher, ob es möglich ist, diese Methode in einer selbst gehosteten (Konsolenanwendung) Umgebung zu verwenden oder sie zu verwenden. In einer selbst gehosteten Konsolenanwendung, denke ich, verwaltet die Anwendung selbst die Anfragen und alle Anfragen laufen innerhalb der gleichen AppDomain (Anwendungen Standard-AppDomain, erstellen wir keine neue appDomain), also kann ich nur eine lange laufende Aufgabe in ausführen eine Feuer-und-Vergessen-Mode, ohne etwas Besonderes zu machen?

Wie auch immer, wenn ich QueueBackgroundWorkItem verwenden, erhalte ich immer die Fehlermeldung:

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

Ich fand auch this Frage auf SO und um ehrlich zu sein es ein wenig verwirrend für mich als den einzigen Weg scheint das zu erreichen ist IRegisteredObject oder nicht?

Ich versuche nur, eine lange laufende Aufgabe in einer selbst gehosteten Anwendung zu laufen, und ich schätze irgendwelche Ideen darüber. Fast alle Ressourcen, Fragen, die ich gefunden habe, basieren auf asp.net und ich bin mir nicht sicher, wo ich anfangen soll.

+0

Ist es nicht eine Option, die Aufgabe (in der Datenbank) in die Warteschlange einzureihen und einen Dienst behandeln zu lassen? –

+0

Hangfire (https://www.hangfire.io/) macht etwas ähnliches, soweit ich weiß.Ich glaube jedoch, dass dies ein bisschen anders ist als die Frage. – Deniz

Antwort

2

selbst gehosteten OWIN Anwendungen sind in der Regel regelmäßige Konsolenanwendungen, daher können Sie einige lange laufende Aufgabe in der gleichen Art und Weise ausgliedern Sie, dass in einer Konsolenanwendung tun würde, zum Beispiel:

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

In IIS gehostete ASP.NET-Anwendungen ist es nicht ratsam, solche Verfahren zu verwenden Da Anwendungspools häufig wiederverwendet werden, wird Ihre Anwendung häufig heruntergefahren und neu gestartet. In einem solchen Fall würde Ihre Hintergrundaufgabe abgebrochen werden. Um dies zu verhindern (oder zu verschieben), wurden APIs wie QueueBackgroundWorkItem eingeführt.

Da Sie jedoch selbst gehostet werden und nicht in IIS ausgeführt werden, können Sie einfach die oben aufgeführten APIs verwenden.

2

Sie müssen einen Mechanismus zum Hinzufügen einer Aufgabe zu etwas außerhalb des Controllers bereitstellen.

Normalerweise verwende ich in den Apps, die ich für den tatsächlichen Host verwende, IIS, also habe ich herausgefunden, dass ich "gehostete Prozesse" verwenden könnte, aber in einer Konsolen-App ist es wahrscheinlich viel einfacher.

Der offensichtlichste Ansatz, der in dem Sinne kommt, ist in einer Art globalen Objekt/Singletons, die Ihr DI Framework kann weiß um sicher sein zu passieren ist immer das gleiche Objekt als „Container für den Job“

public class FooController : ApiController 
{ 
    ITaskRunner runner; 

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

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

Der Task-Runner könnte den Job erledigen, indem er wenig mehr als ein einfacher Wrapper um Task ist.Run(), wenn Sie möchten, aber wenn Sie diesen Haken dort haben und übergeben, bedeutet dies, dass nachdem die Anfrage "behandelt" und der Controller bereinigt wurde, die Aufgabe immer noch "im Bereich" ist, damit die Anwendung sie weiter verarbeiten kann versuchen, es aufzuräumen.

2

Ich wollte die Antwort von War kommentieren, aber meine eigene Frage zu beantworten scheint für eine lange Erklärung besser. Dies ist möglicherweise keine vollständige Antwort, tut mir leid.

War's Lösung ist ein möglicher Weg, um diese Funktion zu implementieren, und ich habe etwas ähnliches getan. Erstellen Sie im Grunde einen Aufgabenspeicher, und fügen Sie diese Aufgabe dem Aufgabenspeicher hinzu, wenn eine neue Aufgabe ausgelöst oder abgeschlossen wurde. Ich möchte jedoch einige Punkte erwähnen, weil ich glaube, dass es nicht so einfach ist.

1) Ich stimme der Verwendung eines Dependency-Injection-Frameworks zu, um ein Singleton-Muster zu implementieren. Ich habe 'SingleInstance()' Methode von Autofac dazu verwendet. Wie Sie jedoch in vielen Antworten und Ressourcen im Internet sehen können, ist das Singleton-Muster kein beliebtes Muster, obwohl es manchmal unverzichtbar scheint.

2) Ihr Repository sollte threadsicher sein, daher sollte das Hinzufügen/Entfernen von Aufgaben aus diesem Aufgabenspeicher threadsicher sein. Sie können eine gleichzeitige Datenstruktur wie 'ConcurrentDictionary' anstelle von Sperren (was eine teure Operation in Bezug auf CPU-Zeit ist) verwenden.

3) Sie könnten die Verwendung von CancellationToken für Ihr Async in Erwägung ziehen. Operationen. Es ist sehr wahrscheinlich, dass Ihre Anwendung vom Benutzer heruntergefahren wird oder eine Ausnahme, die während der Laufzeit behandelt werden kann. Lang andauernde Aufgaben können ein Engpass für ordnungsgemäßes Herunterfahren/Neustarten sein, insbesondere wenn Sie sensible Daten verlieren und verschiedene Ressourcen wie Dateien verwenden, die in diesem Fall möglicherweise ordnungsgemäß geschlossen werden. Obwohl asynchron. Methoden können nicht sofort abgebrochen werden, wenn Sie versuchen, die Aufgabe mit dem Token abzubrechen, es lohnt sich trotzdem, CancellationToken oder ähnliche Mechanismen auszuprobieren, um Ihre lang andauernden Aufgaben bei Bedarf zu stoppen/abzubrechen.

4) Das Hinzufügen einer neuen Aufgabe zu Ihrem Datenspeicher ist nicht genug, außerdem müssen Sie die Aufgabe entfernen, wenn sie erfolgreich abgeschlossen wurde oder nicht. Ich habe versucht, ein Ereignis auszulösen, das den Abschluss der Aufgabe signalisiert, und dann habe ich diese Aufgabe aus dem Aufgabenspeicher entfernt. Ich bin jedoch nicht damit zufrieden, da das Auslösen eines Ereignisses und das anschließende Binden einer Methode an dieses Ereignis, um eine Aufgabe abzuschließen, mehr oder weniger wie eine Fortsetzung in TPL ist. Es ist wie das Rad neu zu erfinden und auch Ereignisse können in einer Multithread-Umgebung hinterhältige Probleme verursachen.

Hoffe, das hilft. Ich bevorzuge es, einige Erfahrungen auszutauschen, anstatt Code zu veröffentlichen, weil ich nicht glaube, dass ich eine ultimative Lösung für dieses Problem habe. Die Antwort von War gibt eine ungefähre Vorstellung, aber Sie müssen viele Probleme berücksichtigen, bevor Sie dies in einem produktionsfertigen System tun.

Edit: Für lange laufende Aufgaben können Sie sich this Thread anschauen. Es empfiehlt sich, den Thread-Pool zu verwalten.

+0

Yeh meine Gedanken genau ... Ich habe versucht, meine Antwort nicht mit zu viel Hintergrundinformationen, die Annahmen über Ihre Implementierung machen können oder nicht. Für eine vollständige Lösung sollten Sie vermeiden, etwas ganz so Einfaches zu tun, um die Muster/Codierungsstandards im Resto Ihrer Codebasis zu erfüllen. Was Threading-Probleme angeht, sollte Ihre Datenschicht/Geschäftslogik damit umgehen. Daher wäre ich auf dieser Ebene nicht besorgt, Threads/Aufgaben zu starten. Mein 2er ... froh, dass du es gelöst hast :) – War

3

Dies ist genau das, was Hangfire erstellt wurde (https://www.hangfire.io/).

Klingt wie ein Feuer-und-vergessen-Job.

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