Nutzungsvorgaben Task Scheduling

1. Verwendung der Bibliothek isy-task

In diesem Abschnitt werden Inhalt und Einsatz der Bibliothek isy-task beschrieben.

1.1. Maven

Um die Bibliothek einzubinden, fügen Sie die isy-task Abhängigkeit in Ihre pom.xml hinzu.

<dependency>
    <groupId>de.bund.bva.isyfact</groupId>
    <artifactId>isy-task</artifactId>
</dependency>

1.2. Erstellung eines Tasks

Um einen Task zu erstellen, muss eine Methode mit @Scheduled annotiert werden. Dabei handelt es sich um eine Annotation, die eine zu planende Methode kennzeichnet. Es muss genau eines der Attribute cron(), fixedDelay() oder fixedRate() angegeben werden.

Die annotierte Methode darf keine Argumente erwarten. Sie hat normalerweise den Rückgabetyp void; andernfalls wird der zurückgegebene Wert beim Aufruf durch den Scheduler ignoriert.

Die Verarbeitung von mit @Scheduled annotierten Methoden wird durch die mit @EnableScheduling annotierte Konfigurationsklasse IsyTaskAutoConfiguration sichergestellt.

Die @Scheduled-Annotation kann beliebig oft verwendet werden.

@Timed(value = "task.scheduledTasks.scheduleTaskWithFixedDelay")
@Scheduled(fixedDelay = 2000)
public void scheduleTaskWithFixedDelay() {
    System.out.println("Fixed delay task - " + System.currentTimeMillis() / 1000);
}

In diesem Fall ist die Dauer zwischen dem Ende der letzten Ausführung und dem Beginn der nächsten Ausführung festgelegt. Der Task wartet immer, bis der vorhergehende beendet ist.

Diese exemplarische Option sollte verwendet werden, wenn es zwingend erforderlich ist, dass die vorherige Ausführung abgeschlossen ist, bevor sie erneut ausgeführt wird.

Weitere Informationen über den Einsatz von Annotationsattributen finden sich in der Spring Dokumentation.

Damit eine @Scheduled-Annotation von Spring registriert wird, muss die Klasse, in welcher der Task erstellt wird, entweder mit @Component annotiert werden oder über eine @Bean-Methode in den Kontext gelegt werden.

Das Starten und Stoppen eines Tasks wird durch Einsatz der Annotationen vollständig von Spring übernommen.

Tasks sind durch die Autokonfiguration von isy-task so konfiguriert, dass sie im Batch-Profil nicht gestartet werden. Für Tasks, welche nicht die isy-task Autokonfiguration nutzen, muss dies mit der Annotation @Profile("!batch") im jeweiligen Task direkt konfiguriert werden.

1.3. Manuelles Starten und Stoppen von Tasks

Es ist ebenfalls möglich, Tasks händisch zu starten und/oder zu stoppen. Zu diesem Zweck wurde isy-task um die Annotation OnceTask erweitert. Darüber hinaus kommt der TaskScheduler aus dem Spring-Framework zum Einsatz.

Eine Klasse, die einen Task bzw. eine Funktion beinhaltet, die manuell gestartet und gestoppt werden soll, muss zunächst mit @Component annotiert werden und das Runnable-Interface implementieren, damit die mit @OnceTask annotierte Methode von isy-task verarbeitet werden kann.

@Component
public class ProgrammaticallyScheduledTask implements Runnable {

    private static final IsyLogger logger = IsyLoggerFactory.getLogger(ScheduledTasks.class);

    private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");

    @Override
    @OnceTask
    public void run() {
        for (int i = 0; i < 3; i++) {
            try {
                MILLISECONDS.sleep(100);
                logger.info(LogKategorie.JOURNAL, "EISYTA99994", "Manual Task {} :: Execution Time - {}", i, dateTimeFormatter.format(LocalDateTime.now()));
            } catch (InterruptedException e) {
                logger.debug("Thread unterbrochen");
                return;
            }
        }
    }
}

Exemplarisch kann der Task anschließend durch den Aufruf taskScheduler.scheduleWithFixedDelay(task, Duration.ofSeconds(3)); gestartet werden. schedule() liefert einen Rückgabewert vom Typ ScheduledFuture, welcher unter anderem die cancel()-Methode bereitstellt und ein manuelles Stoppen des Tasks ermöglicht.

Im folgenden Beispiel wird ein Task mit einer Verzögerung von jeweils drei Sekunden ausgeführt. Nach zehn Sekunden wird der Task durch die cancel()-Methode manuell gestoppt. Folglich wird der Task dreimal erfolgreich ausgeführt.

@RunWith(SpringRunner.class)
@Import(ProgrammaticallyScheduledTask.class)
@SpringBootTest(classes = TestConfig.class, webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = { "isy.logging.anwendung.name=test", "isy.logging.anwendung.typ=test",
    "isy.logging.anwendung.version=test",
    "isy.task.tasks.programmaticallyScheduledTask-run.host=.*"})
public class TestTaskScheduledProgrammatically {

    @Autowired
    private TaskScheduler taskScheduler;

    @Autowired
    private ProgrammaticallyScheduledTask task;

    @Test
    public void testScheduleManuell() throws Exception {

        String className = ProgrammaticallyScheduledTask.class.getSimpleName();
        String annotatedMethodName = "run";

        ScheduledFuture<?> scheduledFuture =
            taskScheduler.scheduleWithFixedDelay(task, Duration.ofSeconds(3));

        SECONDS.sleep(10);

        scheduledFuture.cancel(true);

        Counter successCounter = TaskCounterBuilder.successCounter(className, annotatedMethodName, registry);

        assertEquals(3, successCounter.count());

    }
}

Erweiterte Funktionalität: Es muss nicht zwangsläufig das Runnable-Interface implementiert werden, alternativ kann eine beliebige Methode mit @OnceTask annotiert werden. Diese muss anschließend per Lambda dem Scheduler übergeben werden:

@RunWith(SpringRunner.class)
@Import(ProgrammaticallyScheduledTask.class)
@SpringBootTest(classes = TestConfig.class, webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = { "isy.logging.anwendung.name=test", "isy.logging.anwendung.typ=test",
    "isy.logging.anwendung.version=test",
    "isy.task.tasks.programmaticallyScheduledTask-run.host=.*"})
public class TestTaskScheduledProgrammatically {

    @Autowired
    private TaskScheduler taskScheduler;

    @Autowired
    // has an @OnceTask-annotated execute()-Method
    private AlternativeTask task;

    @Test
    public void testScheduleManuell() throws Exception {

        ScheduledFuture<?> schedule = taskScheduler.scheduleWithFixedDelay(() -> task.execute(), Duration.ofSeconds(3));

        // ...

        schedule.cancel(true);

    }
}

1.4. Konfigurieren von Tasks

Tasks können über zwei Wege konfiguriert werden. Bevorzugt sollten Tasks über die application.properties konfiguriert werden.

Das folgende Listing zeigt die Konfiguration für einen scheduleTaskWithFixedDelay Task.

isy.task.tasks.scheduledTasks-scheduleTaskWithFixedDelay.deaktiviert={true/false}
isy.task.tasks.scheduledTasks-scheduleTaskWithFixedDelay.host={host}
isy.task.tasks.scheduledTasks-scheduleTaskWithFixedDelay.oauth2-client-registration-id={id}

Zu beachten ist die Konvention zur Namensgebung für Tasks. Sie folgt immer dem Schema "klassenName-methodenName".

ComputerName ist der Name der Maschine, auf der der Task läuft.

Eine weitere Möglichkeit besteht über die programmatische Konfiguration. Hierzu muss, bevor der Task geschedulet wird, die TaskConfig bearbeitet werden und anschließend der Task-Map zusammen mit der Task ID wieder hinzugefügt werden.

Ein Task wird grundsätzlich als Spring Bean konfiguriert.

public void configureTasks(IsyTaskConfigurationProperties cp) {
    taskConfig = cp.getTasks().computeIfAbsent(taskId, k -> new TaskConfig());

    taskConfig.setDeaktiviert("...");
    taskConfig.setHost("...");
    taskConfig.setOauth2ClientRegistrationId("...");
}

1.4.1. Automatische Aktualisierung der Konfiguration

Deprecation

Dieser Teil der Dokumentation ist veraltet und wird in einem zukünftigen Release entfernt. Die Inhalte sollten zur Entwicklung neuer Anwendungen nicht mehr berücksichtigt werden.

Die automatische Prüfung und Aktualisierung der Konfiguration geschieht standardmäßig durch einen Task in der Bibliothek isy-konfiguration, welche von isy-task eingebunden wird.

@Scheduled(fixedDelay = 300, timeUnit = TimeUnit.SECONDS)
public void execute() {
    konfiguration.checkAndUpdate();
}

2. Absicherung von Tasks

Für die Absicherung eines Tasks benötigt es lediglich eine OAuth 2.0 Client Registration ID, um die erforderlichen Sicherheitsmaßnahmen zu implementieren. Hierbei kann der Sicherheitsbaustein entweder Resource Owner Password Credentials (ROPC) oder Client Credentials verwenden. Die Verwendung von ROPC ermöglicht es, die Identität des Benutzers abzufragen und die Tasks sicher auszuführen, während der Client Credentials Flow die Authentifizierung und Autorisierung der Anwendung selbst ermöglicht, um auf geschützte Ressourcen zuzugreifen. Detaillierte Informationen zur Implementierung und Konfiguration dieser Sicherheitsbausteine finden sich in der Spring Boot Security Dokumentation, die eine umfassende Anleitung und Best Practices bereitstellt.

3. Konfigurationsschlüssel

Die folgenden Konfigurationsschlüssel werden von isy-task eingelesen und verwertet.

3.1. Allgemeine Konfiguration

Die ID der Client Registration aus isy-security zur Authentifizierung, wenn keine Task-spezifische oauth2-client-registration-id konfiguriert wird. Hierüber wird der zu verwendende OAuth 2.0 Client und die Kennung, das Passwort und das BHKNZ des Nutzers aufgelöst:

isy.task.default.oauth2-client-registration-id={default-id}

Der Host, wenn kein Task-spezifischer Host konfiguriert wird:

isy.task.default.host={default-host}

3.2. Aufgabenspezifische Konfiguration

Die ID der Client Registration aus isy-security, die zur Authentifizierung genutzt wird. Hierüber wird der zu verwendende OAuth 2.0 Client und die Kennung, das Passwort und das BHKNZ des Nutzers aufgelöst:

isy.task.tasks.<Task>.oauth2-client-registration-id={id}

Der Name des Hosts auf dem der Task ausgeführt werden soll. Der Name kann als regulärer Ausdruck angegeben werden, es wird dann geprüft, ob der tatsächliche Hostname dem regulären Ausdruck entspricht. Dadurch kann auch eine Liste von Hostnamen angegeben werden, z.B. host1|host2|host3:

isy.task.tasks.<Task>.host={host}

Actuator Monitoring-Endpunkte für Micrometer

management.endpoints.web.exposure.include=info,health,metrics

Monitoring mit Actuator ermöglichen

management.endpoint.metrics.enabled=true

4. Monitoring

isy-task stellt folgende Task-spezifische Metriken über den Endpunkt /actuator/metrics zur Verfügung.

Tabelle 1. Metriken
Metriken Beschreibung Namespace

Timer Metriken

Metriken die das Timing von mit @Timed-annotierten Tasks betreffen. Zeigt an, wie oft ein Task ausgeführt wurde, wie viel Zeit alle Durchläufe eines Tasks in Anspruch genommen haben und die maximale Ausführungszeit. Darüber hinaus filtern nach Tags möglich, zum Beispiel: /actuator/metrics/method.timed?tag=method:mySuccessTask

method.timed.

Metriken Task erfolgreich

Zeigt an wie oft ein Task erfolgreich durchgeführt wurde.

className-taskName.success

Metriken Task fehlgeschlagen

Zeigt an wie oft ein Task fehlgeschlagen ist. Ggf. Ausgabe von Exceptions.

className-taskName.failure

Darüber hinaus sind über /actuator/info sowie /actuator/health Informationen über den Zustand des einbindenden Systems verfügbar.

4.1. Monitoring mit ScheduledFuture

Neben dem Monitoring mit Actuator ist ein Monitoring mit Objekten vom Typ ScheduledFuture möglich. Dies ist allerdings nur möglich, wenn ein Task über taskScheduler.schedule() manuell gestartet wurde.

5. Hinweise für den Task im Parallelbetrieb

Bei der Implementierung eines Tasks muss beachtet werden, dass ihn die Bibliothek im Parallelbetrieb betreiben wird. Werden hierbei die Besonderheiten der Java Multithreading API nicht berücksichtigt, kann dies zu einem fehlerhaften Verhalten in der Geschäftsanwendung führen.

5.1. Threadsicherheit

Ein wichtiger Aspekt des Parallelbetriebs ist die Threadsicherheit. In diesem Abschnitt werden die Probleme bezüglich der Threadsicherheit verdeutlicht. Grundsätzlich ist es so, dass Rechner mit mehreren Rechnerkernen, den Parallelbetrieb auf Hardwareebene verwirklichen und somit den Gesamtprozess beschleunigen. Die Anzahl der Rechnerkerne braucht programmatisch aber nicht berücksichtigt werden, weil die Java Laufzeitumgebung auch die Rechenzeit eines einzelnen Rechnerkerns in feingranulare Zeitscheiben schneidet. Hierdurch kann die Rechenzeit einer blockierenden Aufgabe für die Erledigung anderer Aufgaben genutzt werden. Allerdings bietet dies auch ein hohes Potenzial für ein fehlerhaftes Verhalten. Denn die Zuordnung der Zeitscheiben erfolgt bei jeder erneuten Ausführung der Geschäftsanwendung unterschiedlich. Daher kann ein erfolgreicher JUnit-Test eine fehlerfreie Ausführung in der Produktionsumgebung nicht gewährleisten. Selbst die Aufteilung auf unterschiedliche Rechnerkerne verhindert von sich aus kein fehlerhaftes Verhalten. Aus diesem Grund müssen Methoden, die nicht von mehreren Threads gleichzeitig durchlaufen werden sollen, über einen Lock-Mechanismus (beispielsweise über das Schlüsselwort synchronized) davor geschützt werden.

Ein weiteres Problem gemeinsamer Instanzen betrifft die Objektvariablen. Auch der Zugriff auf eine veränderbare Objektvariable (d.h. eine Objektvariable, die nicht mit final versehen wurde) eines gemeinsamen Objekts kann nicht konsistent erfolgen, weil jeder Rechnerkern über einen eigenen Cache verfügt, der sich bei Änderung des Wertes naturgemäß vom Wert im Cache des anderen Rechnerkerns unterscheidet. Hilfreich ist hierbei das Schlüsselwort volatile, das dafür sorgt, dass vor jedem Zugriff eine Synchronisation zwischen dem Thread-spezifischen Cache und dem Hauptspeicher stattfindet. Die Objektvariable die mit volatile versehen wurde, ist also scheinbar atomar. Allerdings trifft das nicht für den schreibenden Zugriff zu, da jegliche Veränderung in mehreren Schritten erledigt wird. Um sicherzustellen, dass der Zugriff auf eine gemeinsame Objektvariable konsistent ist, wird beispielsweise der Wertebehälter einer Ganzzahl mit dem speziellen Wertetypen AtomicInteger definiert. In der Regel wird es sich bei der Objektvariablen aber eher um einen Referenztypen handeln. In diesen Fällen sollten die Objektvariablen in einem ThreadLocal-Objekt deklariert werden.