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
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.
Metriken | Beschreibung | Namespace |
---|---|---|
Timer Metriken |
Metriken die das Timing von mit |
|
Metriken Task erfolgreich |
Zeigt an wie oft ein Task erfolgreich durchgeführt wurde. |
|
Metriken Task fehlgeschlagen |
Zeigt an wie oft ein Task fehlgeschlagen ist. Ggf. Ausgabe von Exceptions. |
|
Darüber hinaus sind über /actuator/info
sowie /actuator/health
Informationen über den Zustand des einbindenden Systems verfügbar.
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.