Zum Inhalt

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.form ist 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

job.log("Hallo vom Zeitplaner!");

2. Wer bin ich, wie wurde ich gestartet?

job.trigger sagt, wodurch der Lauf ausgelöst wurde: cron, interval, once oder manual (von Hand).

job.log("Job: " + job.name);
job.log("Auslöser: " + job.trigger);

3. Datum und Uhrzeit festhalten

job.log("Start: " + new Date().format("dd.MM.yyyy HH:mm:ss"));

4. Etwas zählen

Alle Werkzeuge aus den anderen Kapiteln stehen zur Verfügung — hier db.

var anzahl = db.scalar("SELECT COUNT(*) FROM USERS");
job.log("Benutzer im System: " + anzahl);

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