Abhängigkeitsauflösung und mehrdeutige Abhängigkeiten

Unter bestimmten Umständen wird der Submit eines Batches oder Jobs bei BICsuite oder schedulix mit der Fehlermeldung “Ambigous resolution” verhindert. Wir zeigen Ihnen in diesem Beitrag, wie es zu dieser Fehlermeldung kommen kann und wie Sie Abhängigkeiten so definieren können, damit sie Ihr Service Orchestration and Automation Platform klar und eindeutig zu verarbeiten kann.

Beispiel 1

Beginnen wir mit einem sehr einfachen Beispiel. Jemand möchte eine Verarbeitung für eine Datenbanktabelle durchführen. Dafür müssen Daten extrahiert, transformiert und dann in ein anderes System geladen werden. Es geht dabei nicht so sehr darum, was tatsächlich geschieht, sondern welche Rolle die beteiligten Jobs spielen. Weil drei Arbeitsschritte ausgeführt werden sollen, werden drei Jobs erstellt. Nennen wir sie E, T und L. Offensichtlich wird es einige Abhängigkeiten zwischen den Jobs geben, weil es nicht sinnvoll ist, Daten zu laden, bevor sie überhaupt extrahiert wurden, und das Laden von Rohdaten schlägt ebenfalls fehl.

Also definieren wir den Ablauf der Jobs folgendermaßen:

In dieser Abbildung wird der Batch Master Blau und die Jobs in Gelb dargestellt. Die roten Pfeilen repräsentieren Abhängigkeiten. Eltern-Kind-Beziehungen werden mit schwarzen Linien dargestellt, wobei die Eltern oben und die Kinder unten stehen.

Die Textdefinition sieht im Grunde so aus:

create job definition system.e
with
    type = job,
    …;
create job definition system.t
with
    type = job,
    required = (system.e),
    …;
create job definition system.l
with
    type = job,
    required = (system.t),
    …;
create job definition system.master
with
    type = batch,
    children = (
        system.e,
        system.t,
        system.l
    ),
    …;

Wie aus der Definition hervorgeht, beziehen sich Abhängigkeiten auf die Jobdefinitionen, jedoch nicht auf die Jobs. Das ist kein Problem, weil zum Zeitpunkt des Submits völlig klar ist, welche Jobs Vorgänger und welche Jobs Nachfolger sind.

Nach einiger Zeit stellt sich jedoch heraus, dass die Performance des Job Flows nicht ideal ist. Glücklicherweise ist die Ursprungsdatenbanktabelle partitioniert, deshalb kann die Verarbeitung partitionsweise erfolgen. Natürlich müssen zuvor einige Änderungen am ursprünglichen Ablauf vorgenommen werden.

Die Jobs E, T und L müssen untergeordnete Elemente eines Zwischen-Batches sein, und es ist ein Job erforderlich, der die Anzahl der Partitionen der Ursprungstabelle bestimmt und eine eigene Instanz des Zwischen-Batches für jede Partition startet.

Die gestrichelte Eltern-Kind-Beziehung zwischen Submitter und Intermediate zeigt an, dass Intermediate ein dynamisches Kind seines Elternteils ist. Dies hat zur Folge, dass eine Instanz des untergeordneten Elements nicht zum Zeitpunkt der Übermittlung, sondern während der Laufzeit des übergeordneten Elementes Submitter erstellt wird.

Wenn wir uns die Befehle ansehen, mit denen diese Struktur erstellt wird, sehen wir, dass es keinen großen Unterschied zwischen der ursprünglichen und der erweiterten Implementierung gibt. Was zuvor der Batch Master war, heißt jetzt Intermediate, und zwei weitere Hierarchieebenen wurden hinzugefügt.

create job definition system.e
with
    type = job,
    …;
create job definition system.t
with
    type = job,
    required = (system.e),
    …;
create job definition system.l
with
    type = job,
    required = (system.t),
    …;
create job definition system.master
with
    type = batch,
    children = (
        system.e,
        system.t,
        system.l
    ),
    …;

Wie im wirklichen Leben können Eltern in einem Durchlauf sogar mehrere Kinder erzeugen. Die Anzahl der zu erstellenden untergeordneten Elemente ist in der Definition des Auftragsablaufs nicht sichtbar. Dies ist analog zu einer Schleife in einigen Programmiersprachen. Innerhalb des Programms ist es nicht unbedingt offensichtlich, wie oft der Schleifeninhalt ausgeführt wird. Der Schleifeninhalt “weiß” auch gar nicht, wie viele Aufrufe vor ihm waren und wie viele folgen werden.

Das folgende Bild zeigt den Jobfluss mit zwei dynamischen untergeordneten Elementen (natürlich können beliebig viele Elemente dynamisch erzeugt werden, doch mit dem Hinzufügen von weiteren Elementen würde das Beispiel nicht verständlicher werden, weil der Ablauf analog ist).

Hier sehen wir das Hierarchie- und Abhängigkeitsdiagramm des Jobflusses zur Laufzeit. Obwohl in der Definition nicht die genauen Abhängigkeiten zwischen den Jobs angegeben wurden, kann das System die richtigen und beabsichtigten Abhängigkeiten erstellen. Es wäre nicht sinnvoll, wenn T(1) warten würde, bis E(2) fertig ist. Und da die Partitionen unabhängig voneinander verarbeitet werden können, wäre es auch nicht sinnvoll, wenn T(2) auf E(1) warten würde.

Unter der Haube sucht das System dynamisch nach dem am besten passenden Job, wenn es beim Senden eines (Teil-) Baums auf eine Abhängigkeitsdefinition stößt. Und der am besten passende Job wird als die nächstgelegene Instanz der Jobdefinition definiert, nach der gesucht wird. Im obigen Beispiel ist es offensichtlich, dass der Abstand zwischen T(1) und E(2) größer ist als der Abstand zwischen T(1) und E(1). Aus diesem Grund wird zwischen den beiden letzteren eine Abhängigkeitsinstanz erstellt.

Kehren wir zur anfänglichen Implementierung des Jobflusses zurück und gehen davon aus, dass ein Bericht erstellt werden muss. Dies muss natürlich geschehen, nachdem das Laden der Daten erfolgreich abgeschlossen wurde. Das Bild sieht jetzt so aus:

Daran ist noch nichts Besonderes. Aber wenn wir jetzt die dynamische Übermittlungsfunktion hinzufügen, um die Leistung durch die partitionsweise Verarbeitung der Tabelle zu verbessern, müssen wir vorsichtig sein. Wir können die Abhängigkeit zwischen L und dem Report nicht einfach so lassen, wie sie ist. Innerhalb der Definition würde es immer noch vernünftig aussehen und tatsächlich ist es möglich, den Jobfluss so zu definieren.

Was jedoch sofort ins Auge fällt, ist, dass die Abhängigkeit eine Hierarchie übergreifende Abhängigkeit ist. Jobs auf verschiedenen Hierarchieebenen scheinen miteinander verbunden zu sein.

Bei der Programmierung weisen solche Strukturen häufig auf Konstruktionsfehler hin. Und wenn ein Ergebnis, das mehrere Ebenen tiefer erzeugt wird, für den nächsten Schritt relevant ist, besteht die richtige Vorgehensweise darin, dieses Ergebnis in die obere Ebene zu transportieren.

Es wäre sogar möglich, diesen Ablauf zu senden und auszuführen, solange nur eine einzige Partition verarbeitet werden muss. In diesem Fall müsste die Abhängigkeit Unresolved Mode Defer definiert werden. Aber sobald man versucht, eine zweite Instanz des Zwischenstapels einzureichen, werden die Dinge verwirrend, da plötzlich nicht mehr klar ist, auf welche Instanzen von L der Bericht warten soll.

Beide Instanzen von L haben den gleichen Abstand zu Report, dehalb meldet das System eine mehrdeutige Auflösung. Im Bild werden die problematischen Abhängigkeiten durch die gestrichelten roten Linien dargestellt und mit Fragezeichen markiert.

In diesem Beispiel kann das Problem leicht behoben werden. Der Job Report muss warten, bis alle L-Jobs erfolgreich abgeschlossen wurden. Tatsächlich muss er warten, bis der Job Submitter und alle seine Kinder abgeschlossen sind. Anstatt eine Abhängigkeit zwischen L und Report zu erstellen, wird einfach eine Abhängigkeit zwischen Submitter und Report erzeugt.

Beispiel 2

Es wäre ein Fehler zu glauben, dass eine mehrdeutige Abhängigkeitsauflösung nur dann auftreten kann, wenn Jobs mithilfe dynamischer Übermittlungen oder Trigger zur Laufzeit instanziiert werden. Es gibt noch weitere Möglichkeiten, dergeartete Probleme zu verursachen. Nehmen wir in diesem Beispiel an, es gibt zwei Jobs, A und B, und ein generisches Dienstprogramm U. Sowohl A als auch B benötigen dieses Dienstprogramm. Der Job Flow (der sich in der Vergangenheit so bewährt hat) sieht folgendermaßen aus:

Und das wird tatsächlich wie geplant funktionieren. Mit dieser Definition gibt es kein Problem. Sowohl A als auch B finden die richtige und beabsichtigte (nächste) Instanz von U. Wenn aber nun das Dienstprogramm U einen Vorgänger benötigt, möglicherweise für eine Art Bereinigung oder andere Vorbereitungen, sieht es anders aus.

Angenommen, jemand fügt diesen Job hinzu und formt den Jobfluss neu, um ihn „schön“ zu machen:

Jetzt beginnen die Probleme! Es gibt keine Möglichkeit, A oder B mitzuteilen, welche der Instanzen von U sie benötigen. Innerhalb eines Elternteils ist nicht definiert, wie die Kinder sortiert werden. Oft werden die Kinder eines Elternteils in alphabetischer Reihenfolge angezeigt. Das macht es aber nur einfacher, ein bestimmtes Kind zu finden und hat keine andere Bedeutung.

Aus der Sicht der Eltern-Kind-Beziehung entspricht der Abstand zwischen dem A Utility und A genau dem Abstand zwischen A Utility und B, deswegen gibt es keinen Unterschied zwischen den Abständen von A und jeder der Instanzen von U.

Beim Versuch, diesen Jobablauf zu submitten, lehnt das System dies ab und gibt eine Fehlermeldung aus, die auf eine mehrdeutige Abhängigkeitsauflösung hinweist.

Dies kann wiederum behoben werden, indem eine Abhängigkeit zwischen B Utiliy und B und A Utility und A erzeugt wird.