Laden...

Wiederkehrende Tasks in Datenbank speichern und ausführen lassen

Erstellt von fichz vor 5 Jahren Letzter Beitrag vor 5 Jahren 3.130 Views
F
fichz Themenstarter:in
26 Beiträge seit 2013
vor 5 Jahren
Wiederkehrende Tasks in Datenbank speichern und ausführen lassen

Hallo Leute,

mir ist leider kein passender Titel eingefallen darum versuche ich mein Problem hier etwas zu erläutern:

Es soll ein Scheduler erstellt werden welcher zu bestimmten Zeitpunkten "Tasks" starten soll.
Diese verschiedenen "Tasks" werden in einer Datenbank - SQLServer - gespeichert.

Solch ein Task beinhaltet unter anderem die Felder wie Id, Beschreibung, Timestamps, Parameter, etc..
Diese Task können unterschiedliche Funktionen ausführen:- Starten einer Batchdatei (.cmd) (kann unter Umständen zusätzliche Parameter beinhalten)

  • Download einer Datei eines FTP-Servers (benötigt Usernamen und Passwort)
  • Auswerten einer Logdatei (Pfad zur Logdatei)
  • Versenden eines E-Mails (E-Mail EMpfänger, Betreff, ...)

Alle diese Funktionen müssen ein Interface (IFunction) implementieren welches im Prinzip nur 2 Member beinhaltet:


public interface IFunction
{
	string Description { get; }
	Task PerformAsync();
}

Eine Implementierung dieses wäre zB.:


public class DelayedTestFunction : IFunction
{
	public string Description
	{
		get { return "Testfunktion mit einem Delay von 3 Sekunden"; }
	}

	public async Task PerformAsync()
	{
		await Task.Delay(3000);
		System.Diagnostics.Debug.Print("Testfunktion");
	}
	
}

Das Hauptprogramm hält nun eine Liste mit IFunctions und führt diese dann zu bestimmten Zeitpunkten aus.

Nun zum Problem:
Wie kann ich diese Tasks in der Datenbank abspeichern, damit ich diese dann auch wieder laden kann?
Genauer gesagt wie weiß ich, dass z.B.: der Task mit der ID 1 eine "BatchFunction" ist damit ich dieser auch beim Instanziieren die notwendigen Parameter mitgeben kann? Sollte ich hier eine Art Factory erstellen welche anhand eines Identifiers diese erstellt?


public static class FunctionFactory
{
	public static IFunction GetFunction(int id, string parameter)
	{
		switch(id)
		{
			case 1:
				return new DelayedTestFunction();
			case 2:
				return new BatchFunction(parameter);
			default:
				throw new ArgumentException("nix gefunden");
		}
	}
}

Am schönsten wäre es ja, wenn eine neue Funktion hinzugefügt wird, diese ohne große Änderungen/Anpassungen verwendet werden kann.
Gibt es für diese Art "Problem" eine saubere Lösung bzw. eine Art Standard wie man so etwas umsetzen sollte??

Falls noch etwas unklar sein sollte bitte einfach fragen.

lg
fichz

2.207 Beiträge seit 2011
vor 5 Jahren

Hallo fichz,

suchst du etwas wie HangFire?

Ich habe den Titel mal angepasst. Der vorige Titel hat das Problem nicht mal im Ansatz beschrieben. Korrigier es bitte, wenn du was besseres hast.

Gruss

Coffeebean

F
fichz Themenstarter:in
26 Beiträge seit 2013
vor 5 Jahren

Hallo Coffeebean,

danke für die Korrektur, nur leider trifft es das auch nicht so recht was ich meine X(
Danke auch für den Link das könnte ich eventuell sogar verwenden für dieses Projekt.

Mir geht es bei meinem Problem aber weniger um die Funktionalität des Schedulers sondern eher um die Architektur.
Vielleicht ist es besser ich erkläre einmal ganz Simple ausgedrückt was ich vorhabe und wie derzeit denke es lösen zu wollen:

Zum Programm:
Ein Scheduler. Führt zu bestimmten Zeitpunkten Aufgaben aus (Was ein Scheduler halt so macht 😄)
Verschiedene solcher Aufgaben können vom Benutzer gespeichert werden (diese werden in einer SQL-Tabelle hinterlegt). Hierzu muss er angeben (vereinfacht):

  • Beschreibung der Aufgabe
  • Wann bzw. in welchem Intervall sie ausgeführt werden soll
  • WAS ausgeführt werden muss (Aufgabentyp) - Wird in Form einer ComboBox zur Auswahl stehen

Mit geht es konkret um das WAS wo ich mein Problem sehe. Dies kann z.B. eben sein, dass einfach nur eine Batchdatei gestartet werden soll (Typ BatchAufgabe) oder halt auch eine Datei von einem FTP-Server geladen wird (Typ FtpDownloadAufgabe). (Es wird oft passieren, dass neue Aufgabentypen zu einem späteren Zeitpunkt hinzugefügt werden müssen. Als Beispiel fällt mir jetzt spontan ein: Es muss eine Log-Datei alle 10 Minuten analysiert werden und bei einem Fehler soll eine E-Mail versendet werden (LogAnalyseAufgabe))

Diese unterschiedlichen Aufgabentypen muss ich ja auch in der Datenbank speichern damit diese beim nächsten Start wieder laden kann.

Mein Lösungsgedanke:
Diese verschiedenen Aufgabentypen werden in Form einer Klasse dargestellt. Um diese gleich behandeln zu können war meine Idee, alle ein Interface implementieren zu lassen damit es dem Scheduler ja egal sein kann welcher Typ sich wirklich dahinter verbirgt. Der Scheduler muss es nur starten (Perform/PerformAsync). Erweiterbar wäre das Ganze dadurch ja auch, falls neue Aufgabentypen dazu kommen. Da kam mir der Gedanke mit einem Pluginsystem, dass ich theoretisch einen neuen Aufgabentyp in Form einer neuen dll in ein Pluginverzeichnis lege und diese vom Programm dann erkannt wird (darum vielleicht der etwas irreführende Titel des Threads)

Dass der Scheduler seine Arbeit verrichtet ist wie gesagt nicht das Problem. Mein Problem liegt eigentlich darin, dass wenn das Programm gestartet wird, die Aufgabeneinträge aus der Datenbank gelesen werden und ich ja irgendwie Instanzen der einzelnen Aufgabentypen erstellen muss. Doof gesagt: Wie weiß mein Programm, dass Aufgabe 2 eine FtpDownloadAufgabe ist und eine neue FtpDownloadAufgabe-Instanz erstellt werden muss.

Hoffe ich konnte es so ein wenig besser erläutern wo mein Problem liegt. Vielleicht ist mein Lösungsgedanke ja auch falsch und ihr könnt mir vielleicht sagen wie man sowas in der Art besser umsetzen kann.

lg
fichz

2.207 Beiträge seit 2011
vor 5 Jahren

Hallo fichz,

ich würde das mit Dependency Injection lösen. Registrier dir eine Collection von Klassen, die alle ein Interface implementieren und injecte dir die Collection in eine Factory (zum Beispiel) und anhand von einem Identifier suchst du dir die Klasse raus und gibst sie als Interface zurück. Dann kann man wieder die "Process"-Methode aufrufen.

Nur aufpassen, falls die konkrete Klasse enorm viele Abhängigkeiten hat, kann das teuer werden. Du bekommst ja eine Liste deiner Klassen injected. Aber das musst du bei dir konkret schauen.

Gruss

Coffeebean

P
1.090 Beiträge seit 2011
vor 5 Jahren

Schau dir mal die letzten beiden Antworten aus dem Beitrag an.

Wie kann ich in einer If-Abfrage zum nächsten Item übergehen ohne die If-Abfrage zu verlassen?

Mit ein paar kleinen Anpassungen, sollte es bei dir Funktionieren.

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern

D
152 Beiträge seit 2013
vor 5 Jahren

Ich glaube es geht nicht unbedingt um das Ausführen an sich, sondern um das Speichern der Aufgaben mit den spezifischen Parametern.

Speichen den Daten in einer Tabelle mit den Spalten- TaskId

  • Typ
  • Parameter (XML, JSON, ...)
  • Schedule

Für die Einstellungen gibt es eine abstrakte Klasse


public abstract class TaskParameter
{
    public abstract string Serialize();

    public void Deserialize(string parameter);
}

2.207 Beiträge seit 2011
vor 5 Jahren

Hallo zusammen,

den Typ kann man ja in der DB speichern. Mit der ID kann man dann genau sagen, welcher Task welcher Typ sein soll.

Gruss

Coffeebean

M
184 Beiträge seit 2012
vor 5 Jahren

Vielleicht wäre auch ein Scripting-Framework (Roslyn Scripting?) für dich interessant?
So dass du ein entsprechendes Script direkt mit in die DB schreibst.

Das hängt natürlich immer davon ab was du später verarbeiten möchtest und auch das Thema Security dürfte dann wichtiger werden. Dafür wärst du möglicherweise aber sehr flexibel.

T
461 Beiträge seit 2013
vor 5 Jahren

Warum nicht die Lösung diese DLL's aus einem Verzeichnis dynamisch zu laden?
Auf diese Weise bekommst du den konkreten Typen den du auch initiieren kannst, ohne vorher zu wissen was das für einer ist.
(natürlich muß diese DLL das benötigte Interface implemetieren...)

Beim Ausführen werden nur solche verwendet, die konfiguriert, sprich, Infos in der Datenbank vorhanden sind.

Wenn du eine neue hinzufügen möchtest, sollte sie zuerst bei den Einstellungen konfiguriert werden können. Durch das dynamische Laden, kannst einfach alle vorhandenen Assemblies anzeigen und einstellen wie du es benötigts, auch aktivieren oder deaktivieren. Sobald sie in der DB konfiguriert sind, können sie auch ausgeführt werden.

Wie die Konfigurationen aussehen, kann ja egal sein, interessiert ja nur die konkrete Assembly.

Ich habe den Titel mal angepasst, so dass Suchende auch etwas damit anfangen können. EDIT: Ich sollte beim Wort "Shift" im Titel das "f" nicht vergessen... 😄

5.657 Beiträge seit 2006
vor 5 Jahren

Es geht meiner Meinung nach in der Frage lediglich darum, wie man diesen Code durch etwas flexibleres ersetzen kann:


        switch(id)
        {
            case 1:
                return new DelayedTestFunction();
            case 2:
                return new BatchFunction(parameter);
            default:
                throw new ArgumentException("nix gefunden");
        }

Man braucht dafür eine Datenstruktur, in der die Plugins registriert werden können, um die IDs der jeweiligen Implementierung der Action zuzuordnen.

Im einfachsten Fall ist das ein Dictionary<int, IFunction>.

Weeks of programming can save you hours of planning

148 Beiträge seit 2013
vor 5 Jahren

Im Prinzip kannst du es dir doch recht einfach machen. So wie ich das verstehe ist der Code ja komplett in deiner Hand. Deswegen würde ich einfach das IFunction Interface erweitern.


public interface IFunction
{
    Guid ID { get; }
    string Description { get; }
}

public interface IParamFunction<T> : IFunction
{
   Task PerformAsync(T parameters);
}

public interface INonParamFunction : IFunction
{
    Task PerformAsync();
}

Eine Implementierung einer (nicht ganz ernst gemeinten) Funktion könnte dann zum Beispiel so aussehen:

public class ThrowExceptionFunction : IParamFunction<string>
{
   public Guid ID => new Guid("f4f8d277-e59c-459a-a52c-d547b0b11202"); //Die GUID ist natürlich für jede Function neugeneriert!
   public string Description => "Throws an exception";
   public Task PerformAsync(string message) => throw new NotImplementedException(message);
}

In deiner Datenbank speicherst du nun zu jedem Task lediglich noch die ID der Funktion.
Dein "Scheduler" hält eine Liste von Instanzen aller IFunctions (List<IFunction> functionList).
Sobald die Zeit zur Ausführung gekommen ist, benötigst du nur noch ein simples:

var function = functionList.SingleOrDefault(f => f.ID == task.ID);
if(function == null) //DO SOMETHING
else function.PerformAsync(parameters);

Wie du siehst umgehe ich die Factory indem ich die Parameter der PerformAsync Methode übergebe und nicht beim initialisieren der Klasse.

Das serialisieren/deserialisieren deiner Parameter und den check, ob die aktuelle Funktion Parameter reinbekommt oder nicht, habe ich jetzt mal weggelassen. Sollte aber eigentlich keine große Hürde darstellen.

So könnte man es jedenfalls ziemlich einfach umsetzen. Und es wären bei neuen Funktionen keine Anpassungen notwendig.

Grüße

16.806 Beiträge seit 2008
vor 5 Jahren

Die Idee von Coffeebean, mit HangFire, sehe immer noch als die passende Lösung an.

HangFire erkennt das WAS anhang der Methodensignatur. Es ist daher absolut kein Problem viele verschiedene Dinge zu schedulen oder neue hinzuzufügen.
Ich nutze für alle Szenarien, die ich / wir derzeit haben, HangFire.

Natürlich musst Du noch was dazu programmieren, wie eben Deine Auswahlbox etc etc.
Aber alles, was das Scheduling betrifft, passt HangFire.

F
fichz Themenstarter:in
26 Beiträge seit 2013
vor 5 Jahren

Hallo und danke für die zahlreichen Antworten!

Hangfire werde ich mir bezüglich des Schedulers auf jeden Fall mal ansehen.
Da es mir aber prinzipiell darum ging wie man sich solch eine Architektur aufbaut kann ich mit diesen Antworten schon einiges anfangen.

Ich habe auch das Thema MEF für ein Pluginsystem gefunden was ich denk ich hier gut anwenden kann (hoffe es gibt nichts "aktuelleres").

Danke für eure Hilfe 😁

lg