Durchsuchbare Dokumentation aufrufen | Zurück zur Dokumentationsübersicht
Navigation: Dokumentationen agorum core > agorum core für Entwickler > agorum core workflow 3.0
Workflows laufen immer innerhalb einer Transaktion. Dieses Verhalten hat direkte Auswirkungen darauf, wie Fehler bei Objektoperationen (Erstellen, Lesen, Ändern, Löschen) in Workflows behandelt werden dürfen. Dieses Kapitel beschreibt die Hintergründe, die geltende Regel für try/catch in Workflows sowie eine typische Fehlerursache (Race Condition bei createPath) inklusive empfohlener Lösung.
Jede Workflow-Instanz verarbeitet ihre Schritte innerhalb einer Transaktion. Tritt während dieser Transaktion ein Fehler bei einer Objektoperation auf (etwa beim Anlegen, Ändern oder Löschen eines Objekts), wurde die Operation typischerweise nur teilweise in die Transaktion geschrieben, bevor der Fehler geworfen wurde. Die Transaktion befindet sich ab diesem Zeitpunkt in einem inkonsistenten Zustand.
Damit dieser Zustand nicht committet wird, muss der Fehler bis zur umgebenden Transaktionsklammer durchgereicht werden. Erst dadurch wird die Transaktion geregelt zurückgerollt (Rollback) und die teilweise geschriebenen Änderungen werden verworfen.
Achtung: Fängt ein try/catch in einer Transaktion (und damit in einem Workflow) einen Fehler bei einer Objektoperation, muss der Fehler im catch-Block mit throw weitergeworfen werden. Ansonsten wird die Transaktion regulär committet, inklusive der teilweise geschriebenen Änderungen aus dem try-Block. Das führt zu inkonsistenten Daten.
Aus der Rollback-Logik ergibt sich eine wichtige Folgerung für den catch-Block: Alle Schreiboperationen (Erstellen, Ändern, Löschen), die dort ausgeführt werden, werden beim anschließenden Rollback ohnehin wieder zurückgerollt. Sie sind also wirkungslos. Solcher Code ist im besten Fall überflüssig, im schlimmsten Fall irreführend, weil er suggeriert, der Fehler ließe sich hier „reparieren“.
Zusammengefasst:
Beispiel für eine korrekte Fehlerbehandlung in einem Skript-Knoten:
/* global sc, sca, token, instance, outlets, inlet, parameters */
let objects = require('common/objects');
try {
objects.find('/pfad/zum/basisordner').createPath('/Unterordner/2025/07');
} catch (e) {
// Logging zur Fehlerdiagnose ist sinnvoll und unproblematisch
console.log('Fehler beim Anlegen des Pfads: ' + e);
// Der Fehler muss weitergeworfen werden, damit die Transaktion
// zurückgerollt wird. Sonst wird sie mit den halben Änderungen
// aus dem try-Block committet und die Daten sind inkonsistent.
// Weitere Schreiboperationen wären hier ohnehin wirkungslos,
// da sie beim Rollback verworfen werden.
throw e;
}
token.leave('');
Für Fehlermeldungen, die einem Benutzer in der Bedienoberfläche angezeigt werden sollen, steht der Knoten Fehler zur Verfügung.
Beim parallelen Durchlauf mehrerer Workflow-Instanzen tritt sporadisch folgender Fehler auf:
Duplicate entry '<ID>-<ordnername>' for key 'FOLDERPATHRELATIONOBJECT_UNI'
Der Fehler tritt auf, wenn im Workflow-Code der Knoten Pfade anlegen beziehungsweise die Methode createPath() verwendet wird, um datumsbasierte oder anderweitig dynamische Ordnerstrukturen zu erstellen (z. B. Monatsordner wie /Ansichten/2025/07).
Typisches Verhalten: Der Workflow schlägt fehl und das Objekt landet im Fehler-Ordner. Beim erneuten Starten läuft der Workflow fehlerfrei durch, da der Ordner dann bereits existiert.
Existiert der Pfad bereits, gibt createPath() den vorhandenen Ordner einfach zurück, ein zweiter Aufruf mit demselben Pfad ist also unproblematisch. Das gilt jedoch nur innerhalb einer einzelnen Transaktion. Bei parallelen Workflow-Instanzen laufen die Aufrufe in getrennten Transaktionen. Zwischen der internen Existenzprüfung und dem tatsächlichen Anlegen des Ordners kann ein anderer Thread den Ordner bereits erstellt haben.
Es handelt sich also um eine Race Condition (Wettlaufsituation):
Das Problem tritt besonders häufig auf bei:
Ein naheliegender Ansatz wäre, den Aufruf von createPath() in einen try/catch-Block zu wrappen und bei einem Duplicate entry-Fehler den bereits existierenden Ordner per objects.find() zu holen.
Achtung: Dieser Ansatz funktioniert in Workflows nicht. Der Duplicate entry-Fehler wird zwar geworfen, nachdem createPath() intern bereits Zwischenobjekte in die laufende Transaktion geschrieben hat. Um sie geregelt zurückzurollen, muss der Fehler aber bis zur Transaktionsklammer durchgereicht werden. Jede weitere Schreiboperation im catch-Block wird durch diesen Rollback wieder verworfen; jede Leseoperation wie objects.find() arbeitet auf einem inkonsistenten Zustand und liefert kein verlässliches Ergebnis. Der Ordner lässt sich innerhalb dieses Workflow-Durchlaufs nicht mehr sinnvoll ermitteln.
Die korrekte Lösung besteht darin, die benötigten Ordnerstrukturen außerhalb des Workflows vorab anzulegen, sodass der Workflow sie nur noch abrufen muss.
Ansatz: Nächtlicher Task zur Ordnererstellung
Ein separater Task (z. B. CronJob, agorum.task oder ScriptWorker) läuft regelmäßig (z. B. einmal nachts) und erstellt die Ordnerstrukturen für den aktuellen und gegebenenfalls den nächsten Monat im Voraus.
Vorteile dieses Ansatzes:
Ablauf im Workflow
Im Workflow selbst wird weiterhin der Knoten Pfade anlegen beziehungsweise createPath() verwendet. Da der Ordner in der Regel bereits existiert, gibt createPath() einfach den vorhandenen Ordner zurück, ohne einen neuen anzulegen. Dadurch tritt die Race Condition nicht mehr auf.
// Im Workflow: createPath gibt den existierenden Ordner zurück
let targetFolder = objects.find('/pfad/zum/basisordner').createPath('/Unterordner/2025/07');
Beispiel: Nächtlicher Task
// Separater Task (z. B. CronJob), der nachts läuft
let objects = require('common/objects');
let templates = require('common/templates');
let baseFolder = objects.find('/pfad/zum/basisordner');
// Aktuellen Monat erstellen
let currentPath = templates.fill('/Unterordner/${date:yyyy}/\'${date:MM}\'', {});
baseFolder.createPath(currentPath);
// Nächsten Monat ebenfalls erstellen (Vorsorge für Monatswechsel)
let nextMonth = new java.util.GregorianCalendar();
nextMonth.add(java.util.Calendar.MONTH, 1);
let nextMonthPath = '/Unterordner/'
+ nextMonth.get(java.util.Calendar.YEAR)
+ '/'
+ String('0' + (nextMonth.get(java.util.Calendar.MONTH) + 1)).slice(-2);
baseFolder.createPath(nextMonthPath);
Tipp: Werden in einem Workflow datumsbasierte oder dynamische Ordnerstrukturen benötigt, sollten diese immer vorab durch einen separaten, unabhängigen Prozess erstellt werden. Der Workflow selbst sollte die Ordner nur noch abrufen. createPath() im Workflow dient dann nur als Fallback und nicht als primärer Erstellungsmechanismus.