Zeitgesteuerte Jobs¶
Ein Job ist ein Skript, das nicht beim Ausfüllen eines Formulars läuft, sondern nach Zeitplan: jede Nacht aufräumen, jeden Montag einen Bericht verschicken, alle fünf Minuten ein Verzeichnis einlesen.
Eingerichtet werden Jobs unter System-Administration → Betrieb &
Überwachung → Zeitplaner. Ausführliches dazu:
Zeitgesteuerte Skripte (job).
Was im Job anders ist
- Es gibt kein Formular und keinen angemeldeten Benutzer:
ctx.formist leer, Dialoge erreichen niemanden,pdf.render()geht nicht. - Dafür gibt es das Objekt
job:job.log(…)schreibt in die Lauf-Historie — das ist deine einzige Sicht darauf, was passiert ist. - Ein Job darf deutlich länger laufen als ein Formular-Skript (Standard: 5 Minuten).
Ausprobieren
Job anlegen, Skript einfügen, speichern und „Jetzt ausführen" klicken — das geht auch bei deaktivierten Jobs. Die Ausgabe steht danach in der Historie.
Einsteiger¶
1. Hallo Welt¶
2. Wer bin ich, wie wurde ich gestartet?¶
job.trigger sagt, wodurch der Lauf ausgelöst wurde: cron, interval,
once oder manual (von Hand).
3. Datum und Uhrzeit festhalten¶
4. Etwas zählen¶
Alle Werkzeuge aus den anderen Kapiteln stehen zur Verfügung — hier db.
5. Einen Lauf als Fehler markieren¶
job.fail beendet den Job sofort. In der Historie steht der Lauf dann rot als
Fehler — mit deiner Meldung.
job.log("Gleich geht etwas schief …");
job.fail("Absichtlich abgebrochen – nur ein Beispiel");
job.log("Diese Zeile wird nie erreicht");
6. Vorzeitig und ohne Fehler aufhören¶
return beendet den Job — der Lauf gilt als erfolgreich.
var offen = 0; // z. B. aus einer Abfrage
if (offen === 0) {
job.log("Keine offenen Vorgänge – nichts zu tun.");
return;
}
job.log("Verarbeite " + offen + " Vorgänge …");
7. Nur an Werktagen arbeiten¶
Viele Zeitpläne lassen sich so schon im Cron-Ausdruck regeln. Manchmal ist eine Prüfung im Skript übersichtlicher.
var tag = new Date().getDay(); // 0 = Sonntag, 6 = Samstag
var namen = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"];
job.log("Heute ist " + namen[tag]);
if (tag === 0 || tag === 6) {
job.log("Wochenende – der Bericht fällt aus.");
return;
}
job.log("Werktag – Bericht wird erstellt.");
Fortgeschritten¶
8. Ein Bericht per Mail¶
var benutzer = db.scalar("SELECT COUNT(*) FROM USERS");
var zuordnungen = db.scalar("SELECT COUNT(*) FROM USER_GROUPS");
mail.send({
to: "team@beispiel.example",
subject: "Wochenbericht " + new Date().format("dd.MM.yyyy"),
body: "Benutzer: " + benutzer + "\nGruppen-Zuordnungen: " + zuordnungen
});
job.log("Bericht verschickt.");
9. Eine Liste in den Bericht¶
var zeilen = db.query(
"SELECT LOGIN_NAME, EMAIL FROM USERS ORDER BY LOGIN_NAME").slice(0, 10);
var text = "Die ersten " + zeilen.length + " Benutzer:\n\n";
zeilen.forEach(function (z) {
text += "- " + z.login_name + " (" + (z.email || "ohne E-Mail") + ")\n";
});
mail.send({ to: "it@beispiel.example", subject: "Benutzerliste", body: text });
job.log(zeilen.length + " Zeilen im Bericht");
10. Einen Übungsordner vorbereiten¶
Für die folgenden Beispiele legt dieser Job ein Eingangsverzeichnis mit zwei CSV-Dateien an — so, wie sie ein anderes System ablegen würde.
fs["/beispiele/job/eingang"].mkdir(true);
fs["/beispiele/job/archiv"].mkdir(true);
fs["/beispiele/job/eingang/kunden-1.csv"].writeText("name;ort\nMeyer;Hagen\nSchulz;Berlin\n");
fs["/beispiele/job/eingang/kunden-2.csv"].writeText("name;ort\nWagner;Stockholm\n");
job.log("2 Dateien bereitgelegt");
11. Ein Verzeichnis prüfen¶
Fehlt das Verzeichnis, ist etwas grundsätzlich falsch — dann als Fehler beenden, damit es in der Historie auffällt.
var eingang = fs["/beispiele/job/eingang"];
if (!eingang.exists) job.fail("Eingangsverzeichnis fehlt: " + eingang.path);
var dateien = eingang.list(function (f) { return f.name.endsWith(".csv"); });
job.log("Dateien im Eingang: " + dateien.length);
12. Dateien einlesen und ins Archiv verschieben¶
Der klassische Import: jede Datei lesen, verarbeiten, verschieben. So wird keine Datei zweimal eingelesen.
var summe = 0;
fs["/beispiele/job/eingang"].list(function (f) { return f.name.endsWith(".csv"); })
.forEach(function (datei) {
var zeilen = csv.parse(datei, { header: true, separator: ";" });
zeilen.forEach(function (z) {
job.log(" " + z.name + " aus " + z.ort);
});
summe += zeilen.length;
datei.rename("/beispiele/job/archiv/" + datei.name); // verschieben
});
job.log(summe + " Zeilen eingelesen");
13. Alte Dateien löschen¶
Aufräum-Jobs halten Verzeichnisse klein: alles löschen, was älter als eine bestimmte Zahl von Tagen ist.
var grenze = Date.now() - 30 * 24 * 60 * 60 * 1000; // 30 Tage in Millisekunden
var geloescht = 0;
fs["/beispiele/job/archiv"].list().forEach(function (f) {
var alterMinuten = Math.round((Date.now() - f.lastModified) / 60000);
job.log(f.name + " ist " + alterMinuten + " Minuten alt");
if (f.lastModified < grenze) {
f.remove();
geloescht++;
}
});
job.log("Dateien älter als 30 Tage: " + geloescht + " gelöscht");
lastModified ist der Zeitpunkt der letzten Änderung in Millisekunden —
dieselbe Einheit wie Date.now().
14. Einen Webdienst abfragen¶
var r = http.get("http://localhost:8099/api/demo/http/echo");
if (!r.ok) job.fail("Dienst nicht erreichbar: HTTP " + r.status);
job.log("Dienst antwortet: " + r.status);
15. Im Verzeichnis nachschlagen¶
var c = ldap.connect("kcads");
var treffer = c.search("ou=Users,dc=kcforms,dc=test", "(&(objectClass=person)(l=Berlin))");
c.close();
job.log("Personen in Berlin: " + treffer.length);
Profi¶
16. Mitzählen: verarbeitet, übersprungen, fehlerhaft¶
Eine gute Zusammenfassung am Ende zeigt in der Historie auf einen Blick, ob alles glatt lief.
var daten = [
{ nr: 1, betrag: "100" },
{ nr: 2, betrag: "" }, // leer → überspringen
{ nr: 3, betrag: "abc" }, // keine Zahl → Fehler
{ nr: 4, betrag: "250" }
];
var zaehler = { verarbeitet: 0, uebersprungen: 0, fehlerhaft: 0 };
daten.forEach(function (d) {
if (d.betrag === "") { zaehler.uebersprungen++; return; }
if (isNaN(Number(d.betrag))) {
zaehler.fehlerhaft++;
job.log("Satz " + d.nr + ": '" + d.betrag + "' ist keine Zahl");
return;
}
zaehler.verarbeitet++;
});
job.log("Ergebnis: verarbeitet " + zaehler.verarbeitet + ", übersprungen " + zaehler.uebersprungen
+ ", fehlerhaft " + zaehler.fehlerhaft);
17. Ein Fehler soll nicht alles stoppen¶
Mit try … catch pro Datensatz läuft der Job weiter, auch wenn ein
einzelner Satz Probleme macht.
var ok = 0, fehler = 0;
["/beispiele/job/archiv/kunden-1.csv", "/beispiele/job/fehlt.csv", "/beispiele/job/archiv/kunden-2.csv"]
.forEach(function (pfad) {
try {
var text = fs[pfad].readText();
ok++;
} catch (e) {
fehler++;
job.log("Problem mit " + pfad + ": " + e.message);
}
});
job.log(ok + " ok, " + fehler + " Fehler");
18. Nicht doppelt arbeiten¶
Ein Job kann auch zweimal laufen — etwa von Hand gestartet, während der geplante Lauf noch aussteht. Eine Marker-Datei merkt sich, was schon erledigt ist.
var heute = new Date().format("yyyy-MM-dd");
var marker = fs["/beispiele/job/erledigt-" + heute + ".txt"];
if (marker.exists) {
job.log("Bericht für " + heute + " wurde schon erstellt – übersprungen.");
return;
}
// … hier die eigentliche Arbeit …
marker.writeText("erledigt um " + new Date().format("HH:mm:ss"));
job.log("Bericht für " + heute + " erstellt.");
19. Laufzeit messen¶
var start = Date.now();
var n = 0;
db.query("SELECT LOGIN_NAME FROM USERS").forEach(function () { n++; });
job.log(n + " Sätze gelesen. Dauer: " + (Date.now() - start) + " ms");
20. Neuer Versuch bei Störungen¶
Ein Dienst ist kurz nicht erreichbar? Ein paar Versuche mit Pause dazwischen, dann erst aufgeben.
function abrufen(url, versuche) {
for (var i = 1; i <= versuche; i++) {
var r = http.get(url);
job.log("Versuch " + i + ": HTTP " + r.status);
if (r.ok) return r;
java.lang.Thread.sleep(1000 * i); // 1 s, 2 s, 3 s …
}
job.fail("Dienst nach " + versuche + " Versuchen nicht erreichbar: " + url);
}
var antwort = abrufen("http://localhost:8099/api/demo/http/echo", 3);
Warten im Skript
java.lang.Thread.sleep(ms) pausiert das Skript. Das ist im Job in
Ordnung — in Formular-Skripten nicht, dort wartet der Anwender mit.
21. Bericht mit CSV-Anhang¶
var tabelle = new csv.StandardPrinter({ separator: ";", bom: true });
tabelle.println(["Login", "E-Mail"]);
db.query("SELECT LOGIN_NAME, EMAIL FROM USERS ORDER BY LOGIN_NAME").forEach(function (z) {
tabelle.println([z.login_name, z.email]);
});
var mp = new mail.MessageMultipart();
mp.addText("Im Anhang die aktuelle Benutzerliste.");
mp.addBytes("benutzer-" + new Date().format("yyyy-MM-dd") + ".csv",
tabelle.toByteArray(), "text/csv", null);
var msg = new mail.Message();
msg.to = "controlling@beispiel.example";
msg.subject = "Benutzerliste";
msg.content = mp;
mail.send(msg);
22. Alles zusammen: Import mit Protokoll-Mail¶
Einlesen, zählen, archivieren — und am Ende eine Mail nur dann, wenn es Probleme gab.
fs["/beispiele/job/eingang/neu.csv"].writeText("name;ort\nBecker;Hagen\n;Berlin\n");
var probleme = [], gelesen = 0;
fs["/beispiele/job/eingang"].list(function (f) { return f.name.endsWith(".csv"); })
.forEach(function (datei) {
var zeilen = csv.parse(datei, { header: true, separator: ";" });
// Indizierte Schleife wegen der Zeilennummer in der Meldung.
for (var i = 0; i < zeilen.length; i++) {
if (!zeilen[i].name) {
probleme.push(datei.name + ", Zeile " + (i + 2) + ": Name fehlt");
} else {
gelesen++;
}
}
datei.rename("/beispiele/job/archiv/" + datei.name);
});
if (probleme.length > 0) {
mail.send({
to: "import@beispiel.example",
subject: "Import mit " + probleme.length + " Problem(en)",
body: probleme.join("\n")
});
}
job.log("Import fertig: " + gelesen + " gelesen, " + probleme.length + " Problem(e)");
fs["/beispiele/job"].remove(true); // Übungsordner aufräumen