Jobsystem

Einführung

Übersicht

Das Jobsystem ermöglicht in VLS lang-laufende Operationen (Jobs) zu verwalten. Solche Operationen können zum Beispiel Importe von Images oder Updates von vielen Datensätzen sein. Operationen, die nur wenig Zeit benötigen und daher synchron abgearbeitet werden können, werden im Gegensatz zu Jobs als Actions realisiert.

VLS bringt im Core etwa 100 verschiedene Jobs mit. In den Domainverzeichnissen können weitere domain-spezifische Jobs im ./jobs Verzeichnis abgelegt werden.

>>> len(component.vls.job.Jobs)
103

Jobs werden als Python-Klassen implementiert, die von einer Job-Basisklasse abgeleitet sind. Zur Server-Startup-Zeit werden bestimmte Dateien/Verzeichnisse gescanned und alle abgeleiteten Klassen zentral in vls.job.Jobs registriert.

Konfiguration

Das Interface für Jobs wird über ein Konfigurationsformat im ini-style mit Typdeklarationen definiert. Das Format verwendet dieselben Konventionen wie das Format der Server-Konfiguration (server.ini).

class GenerationBackupDatabaseJob(BackupDatabaseJob):
    """Three generation backup of database

    Generation 1: every weekday except Friday, backups are kept for 7 days
    Generation 2: every Friday, backups are kept for 28 days
    Generation 3: last day of every month, backups are kept for one year
    """

    config_ini = """
    # if the database is local and the directory for storing the backup file does not exist
    # we will try to create it (and change permissions for fb process)
    localDB = boolean(default=true)

    # zip the resulting fbk
    zip = option(gz, zip, false, default=gz)

    # keep the unzipped file
    keep = boolean(default=false)
    """

In der Implementierung der Jobklasse lässt sich auf die getypten Werte dann zum Beispiel via self.config.keep zugreifen.

Auf der Adminseite /admin/jobs lässt sich vor dem Starten des GenerationBackupDatabaseJob die Defaultbelegung der Konfigurationsparameter noch verändern:

Beispiel für Jobparameter

Über die HTTP-Schnittstelle lassen sich Jobs über einen Request in der Form /action/startJob?name=GenerationBackupDatabaseJob&keep=true starten. Die Jobkonfiguration wird über GET/POST Parameter gesetzt. Normalerweise werden diese Requests über Aktionen auf dem Adminseiten oder via VLM ausgelöst.

Im Pythonkontext werden Jobparameter als Keywords im Funktionsaufruf gesetzt:

component.vls.job.startJob('GenerationBackupDatabaseJob', keep=True)

Bei verschachtelten Parametern empfiehlt sich folgende Notation:

kwds = {'section1.option1': True, 'section2.option1': 99}
component.vls.job.startJob('GenerationBackupDatabaseJob', **kwds)

Scheduler

Der VLS-Scheduler ermöglicht es, Jobs zu bestimmten Zeitpunkten vom Server ausführen zu lassen. Die Scheduler-Konfiguration findet sich in der server.ini der VL-Instanz. Eine einfaches Beispiel, dass erst den ImportJob und nachfolgend den IndexJob jeden Wochentag um 23:00 ausführt, sieht folgendermaßen aus:

[scheduler]
    [[runImport]]
        eventJob = ImportJob
        eventDomain = ihd
        eventTime = 23:00:00
        eventWeekday = Mo,Tu,We,Th,Fr

    [[runIndex]]
        eventJob = IndexJob
        eventAfter = runImport
        mode = full # the only param that is fed into the job-api (config_ini)

Die Parameter eventDomain, eventJob, eventTime, eventWeekday und eventAfter sind scheduler-spezifische Optionen, die steuern wann der Job in welchem Domainkontext ausgeführt wird. Alle weiteren job-spezifischen Parameter, wie hier mode = full werden an das Jobinterface weitergereicht.

Unter der Adminseite /admin/scheduler lässt sich die aktuelle Konfiguration des Schedulers einsehen:

Beispiel für Scheduler

Threads versus Prozesse

Jobs können entweder als Prozess oder als Thread gestartet werden. In der Defaultkonfiguration werden Jobs als Thread gestartet. Die Defaultkonfiguration eines Jobs kann geändert werden, indem die classlevel Variable Job.External = True gesetzt ist. Zur Laufzeit können Jobs externalisiert werden, indem der Initialisierungsparameter job.external = true gesetzt wird. Unter bestimmten Bedingungen ist es besser Jobs in Prozessen als in Threads zu starten.

Threads

Einen Job threaded zu starten, hat den Vorteil, dass der Job schneller startet. Ein solcher Job startet im Allgemeinen in wenigen Millisekunden wenn die zugehörige Pipeline frei ist. Die Nachteile bestehen darin, dass der Job sich mit dem Hostprozess den Speicher teilt und dass der Python Interpreter nicht über mehrere Cores skaliert (Global Interpreter Lock). Ein harter Crash (Segfault) in einem threaded Job würde auch den Hostprozes terminieren. Zusammengefasst kann man sagen, dass threaded Jobs sich anbieten, wenn:

  • der Job so kurz läuft, dass die Startzeit nicht länger als die Laufzeit sein soll

  • der Job wenig CPU-intensive Operation (im Pythonspace, nicht C-Space wie Image-Transformationen) beinhaltet

  • der Job eher IO (DB-Operationen, Filesystem-Operationen) als CPU-lastig ist

  • der Job keine großen Mengen an RAM alloziert

Prozesse

Wenn ein Job in einem neuem Prozess gestartet wird, dann wir ein neues VLS-Environment auf Basis der Grundkonfiguration (server.ini) hochgefahren. Die Änderungen gegenüber der Grundkonfiguration sind in config.server.modes.job definiert. Jeder Job kann auf class-level über das Dictionary Job.external_cfg die Konfiguration des VLS-Environment feiner auspezifieren.

Das Starten des VLS-Environments dauert mehrere Sekunden, von daher sollten nur Jobs externalisiert werden, die so lange laufen, dass dieser Offset vernachlässigbar ist.

Bei externen Jobs können unter Linux die Niceness-Level des Prozess manipuliert werden. Die Level lassen sich über die Initialisierungsparameter job.cpuPriority und job.ioPriority steuern.

In der server.ini lassen sich unter config.job.external noch weitere Anpassungen an dem Verhalten von externen Jobs konfigurieren.

Administrative Seiten

Einen Überblick über die Jobs, die, in allen Subdomains von einer bestimmten Domain, gelaufen sind findet sich auf der Adminseite /DOMAIN/admin/timeline. Dort wird auch, neben temporalen Eigenschaften wie Laufzeit und Startzeitpunkt oder Erfolg bzw. Fehlschlag, die Hierarchie zwischen verschiedenen Jobs visualisiert.

Job-Timeline

Die Details eines einzelnen Jobs lassen sich auf der Adminseite /admin/jobreport/JOB_ID nachschlagen. Dort lässt sich das generierte Logfile in Plaintext und eine formatierte Ansicht aller vom Job erzeugten Events abrufen.

Pipelines

Die Abarbeitungsreihenfolge von Jobs wird über die Zuordnung zu Jobpipelines bestimmt. Jobs, die auf der selben Pipeline laufen, werden seriell abgearbeitet. So wird zum Beispiel verhindert, dass mehere ImportJobs gleichzeitig versuchen das primäre Importverzeichnis einer Domain abzuarbeiten. Oder, dass mehrere PDFJobs, welche relativ viel Hauptspeicher verbrauchen, gleichzeitig laufen.

Eine Pipeline wird einem Job zugewiesen indem auf class-level Job.Pipeline=PIPELINE_NAME gesetzt wird. Eine neue Pipeline wird eingeführt, indem einfach ein neuer PIPELINE_NAME verwendet wird. Wird keine explizite Pipeline gesetzt, wird der Job der Pipeline mit dem Namen default zugeordnet.

Die Pipeline mit dem Namen parallel ist eine besondere Pipeline, die, im Gegensatz zu allen Anderen, zugeordnete Jobs gleichzeitig abarbeitet.

Zur Laufzeit kann die via class-level gesetzte Default-Pipeline überschrieben werden, indem der Initialisierungsparameter job.pipeline überschrieben wird.

Verwendung in Python

Der folgende Code zeigt das Starten des ImportJob (non-blocking) in bestimmten Domain (ihd) eines bestimmten Server-Kontextes (s2wp\server.ini).

from vls.core import vlsenv
from vls.api import component

plugins = 'vls.import'
with vlsenv('Q:\_server\s2wp\server.ini', plugins):
    component.vls.job.startJob('ImportJob', domainName='ihd')

Bemerkung

Sollte schon ein VLS-Server auf der server.ini laufen, dann muss server.startupChecks = False konfiguriert sein. Sonst wird der vlsenv() Call fehlschlagen.

Der folgende Code zeigt die Implementierung eines Jobs mit Konfigurationsspezifikation:

from vls.core import vlsenv
from vls.api import component

from vls.job import Job

class NewJob(Job):
    config_ini = """
    option1 = integer(default=5)
    # another option that is printed
    option2 = string(default='')
    """
    def _run(self):
        for i in range(self.config.option1):
            print('[%s] %s is Running!' % (i, self.config.option2))

plugins = 'vls.import'
with vlsenv('Q:\_server\s2wp\server.ini', plugins):
    jobcmp = component.vls.job
    jobcmp.Jobs['NewJob'] = NewJob

    jobcmp.startJob('NewJob', option2='NEWJOB')