Spring boot scheduling in depth

In dit artikel behandelen we kort wat scheduling inhoudt en de mogelijkheden die het biedt. Hierna gaan we deze implementeren door middel van Spring Boot. Spring Boot maakt het implementeren van scheduling erg eenvoudig, maar zoals alle autoconfiguratie die Spring Boot biedt, kun je deze aanpassen. Om dit te kunnen gaan we de autoconfiguratie van scheduling onder de loep nemen, waarbij we een goed beeld krijgen van de mogelijkheden. Als laatste behandelen we het testen van de scheduling.

Auteur: Ben Ooms

Scheduling biedt je de mogelijkheid om een stuk code op een geplande moment te laten uitvoeren. Scheduling is onderdeel van Spring context en heeft geen andere dependencies. Theoretisch gezien kun je een Spring Boot applicatie hebben met alleen de kale “spring-boot-starter” die Spring Context als dependency heeft, nadeel is dat de applicatie stopt nadat je deze start. In de sample heb ik daarom gebruikgemaakt van “spring-boot-starter-web” om dit te voorkomen.

De scheduling package biedt wrappers om standard Java SE ScheduledExecutors. Met Spring Boot kun je scheduling activeren door een @Configuration bean op klasse niveau te annoteren met @EnableScheduling. Na scheduling geactiveerd te hebben, kun je Bean methoden annoteren met @Scheduling. Let op dit moeten wel Spring beans zijn, zet je @Scheduling op een methode van een POJO dan gebeurt er niets en de applicatie geeft geen compile of runtime errors. Ook in de logging zal je niets vinden. Dus is dit ook een goede reden om te testen.

Als je scheduling toepast op een Spring boot web project met de actuator starter (spring-boot-starter-actuator), dan heb je onder andere ook een actuator scheduling endpoint tot je beschikking waar je informatie kunt ophalen van alle tasks die gescheduled zijn. Om het endpoint bereikbaar te maken, moet je deze in de properties activeren met management.endpoints.web.exposure.include=scheduledtasks. Op afbeelding 1 zie je hier een screenshot van de sample applicatie van dit artikel met de besproken voorbeelden.

InitialDelay en FixedDelay

Er bestaan twee manieren om de scheduling te specificeren: door een periode op te geven of door een Cron expressie te gebruiken. De eerst benoemde manier, door middel van een periode op te geven, doe je door een @Scheduling annotatie te gebruiken met de parameter fixedRate. De fixedRate accepteert een Long waarde die milliseconden representeert. Er zijn twee opties beschikbaar wanneer je deze manier van specificeren gebruikt. Dit zijn de initialDelay en fixedDelay. Met initialDelay geef je de vertraging van de eerste executie na het starten van de applicatie, met fixedDelay geef je de minimale vertraging tussen twee executies. FixedDelay is ook te gebruiken bij Cron scheduling. Handig om te weten is dat alle vier de parameters ook een variant hebben met de String toevoeging. Met deze variant kan in plaats van een getal in milliseconde een periode opgeven zoals de java.time.Duration klasse accepteert in de parse methode. In listing 1 zie je twee scheduling voorbeelden voor een interval voor 1 minuut, de bovenste gebruikt de Long variant en de onderste de java.time Duration notatie.

Listing 1

De tweede manier is door middel van een Cron expressie. Deze manier zal je moeten gebruiken wanneer je anders dan een interval je een taak wilt inplannen. Stel je voor dat je de taak zoals deze als voorbeeld wilt toepassen, maar dit alleen tijdens kantooruren van 9 tot 17 uur van maandag tot en met vrijdag? Dan kun je dit niet met fixedDelay instellen. Hiervoor gebruik je dan een Cron expressie. In listing 2 zie je dit voorbeeld uitgewerkt.

Listing 2

Let op, wanneer je een Cron expressie gebruikt dat je zes blokken meegeeft: seconden, uren, dagen, maanden, dag van de week. De Unix versie gebruikt meestal maar vijf blokken, de seconden worden niet gebruikt in de Crontab. Dus niet zomaar copy pasten vanuit de bestaande Crontab, dit levert je runtime een IllegalArgumentException op.

Cron expressies

Voor diegene die weinig hebben gedaan met Cron, hierbij een introductie met de meest gebruikte Cron expressies. Zoals gezegd bestaat een Cron expressie uit een String. Van links naar rechts heb je zes posities gescheiden door een spatie. Respectievelijk zijn deze posities seconden, minuten, uren, dag van de maand, maand en als laatste dag van de week. Het meest simpele is een scheduling voor een absoluut tijdstip, als je bijvoorbeeld een taak precies op 1 januari middernacht wilt uitvoeren dan is de expressie 0 0 0 1 1 *.  Voor de maand en dag van de week zijn naast de numerieke specificatie ook letters toegestaan, het voorgaande voorbeeld mag je ook specificeren als “0 0 0 1 JAN”. De specificatie laat toe dat je de eerste drie characters gebruikt van het woord wat je wilt gebruiken, dit mogen dus zowel in hoofdletters als in kleine letters zijn. Maandag schrijf je bijvoorbeeld als MON of mon, december als dec of DEC.

Characters

Naast deze absolute waarden zijn er ook een aantal characters te gebruiken die een speciale betekenis hebben. Met een sterretje op een bepaalde positie geef je aan dat je elke waarde wilt gebruiken van die positie zoals bijvoorbeeld elke minuut of elke dag van de maand. Met een streepje kun je een range meegeven. Als je bijvoorbeeld elke eerste tien dagen van de maand wilt aanduiden kun je dit aangeven met 1-10 in de maand positie. Met een komma kun je een lijst van waarden specificeren, bijvoorbeeld elke maandag, woensdag en vrijdag specificeer je met mon,wed,fri.

Een ander speciaal character is een forward slash /. Deze gebruik je wanneer je periodiek iets wilt laten uitvoeren zoals elke tien minuten */10.

Voor de slash geef je de starttijdstip aan en na de slash de grote van het interval. Als laatste is er een speciale Cron expressie binnen Spring boot die je kunt gebruiken om een scheduling te deactiveren, hiervoor gebruik een “-“. Dit is erg handig als je de scheduling niet hardcode maar deze laat injecteren door middel van een property zoals in Listing 3 te zien is.

Listing 3

Andere opties

Met de besproken Cron expressies kun je de meeste expressies opbouwen. Er zijn nog meer onbesproken opties die gedocumenteerd zijn in de Spring scheduling documentatie. Spring heeft ook wat zij noemen macro expressies. Hiermee kun je shorthand notaties gebruiken als je expressies wilt definiëren zoals elke dag, maand, week of jaar. Hiervoor gebruik je de notatie in het Engels met een apenstaartje ervoor zoals @daily voor dagelijks. Let op: je kunt met deze snelle notatie niet aangeven wanneer je taak uitgevoerd wordt. De shorthand notatie start op het eerst mogelijk moment, dus @daily als voorbeeld is iedere dag middernacht. Alle hier besproken Cron voorbeelden zijn terug te vinden in de Github repository die onderaan het artikel vermeld is in de klasse CronExpressionSamples.

Nu dat wij weten hoe de scheduling te activeren is en te gebruiken is, is de volgende stap de interne werking te behandelen. Spring heeft een aantal klassen als laag boven de kernlaag die hiervoor gebruikt wordt, net zoals Spring JDBC dit biedt boven JDBC met de welbekende JdbcTemplate constructie. Voor Scheduling is dit ook het geval. De Scheduling die je gebruikt in Spring zorgt voor de creatie en sturing van specifieke Executor en Threadpool subklassen onder water. Dit zijn standaard Java klassen uit de java.util.concurrent package.

De auto configuratie wordt uitgevoerd door de TaskSchedulingAutoConfiguration bean. Deze gaat in het classpath op zoek naar twee klassen. De TaskSchedulingBuilder en de ThreadpoolTaskScheduler. De ThreadpoolTaskScheduler is het resultaat van de TaskSchedulingBuilder::build() methode. De TaskSchedulingBuilder is verantwoordelijk voor het opbouwen van de ThreadpoolTaskScheduler. Deze builder gebruikt de TaskSchedulingProperties klasse die de default waarden of de waarden uit meegegeven properties met de prefix “spring.task.scheduling”. Het gaat hier om vier properties, de thread naam prefix (default scheduling-), de maximale thread pool size (default 1), await shutdown (default false) en als laatste de await termination time out (default null!). Geef je geen properties op voor de scheduling, dan zullen al je scheduling methoden uitgevoerd worden door een thread en zal bij het afsluiten van de applicatie ook de scheduling methode die uitgevoerd worden direct mee afgesloten worden.

Listing 4

Dus wanneer je meer dan een scheduling wilt inzetten, moet je op zijn minst de pool size instellen door middel van de property spring.task.scheduling.pool.size. Doe je dit niet, dan kan scheduling al misgaan. Een voorbeeld hiervan zie je in Listing 4. In deze listing zijn twee jobs ingepland om elke minuut te draaien en door middel van een Thread.sleep duren deze 50 seconden. Met de default settings zul je zien in de logging dat de jobs niet altijd om de minuut draaien. Als je in dit voorbeeld de pool size naar 2 configureert, dan werkt het wel als verwacht. In de repository bij dit artikel kun je dit zien in de logging wanneer je de Spring boot applicatie opstart zonder profiel mee te geven en dan weer starten met het profiel “scheduling-set”.

Wil je meer configuratie mogelijkheden dan kun je een @Bean in jouw applicatie aanmaken van het type ThreadpoolTaskScheduler. Als je de source code bekijkt, dan zie je dat dit een subklasse is van ExecutionConfigurationSupport, beide Spring klassen. De ExecutionConfigurationSupport biedt al wat standard mogelijkheden en is een goede basis voor verdere configuratie. Use cases om deze configuratie mogelijkheid te gebruiken zijn:

  • Het overriden van de scheduler service die onder water gebruikt wordt, dit wil je doen als de Spring Boot applicatie draait op een applicatieserver die Threadpools centraal beheert.
  • Het definiëren van policies voor taken die rejected of discarded worden. Als je naar de ThreadPoolExecutor code kijkt zie je een aantal policies die gedefinieerd zijn. Om een policy te definiëren maak je een implementatie van de RejectedExecutionHandler interface. Use case hiervoor is wanneer je meer controle wilt hebben op het gedrag van de executor wanneer taken geweigerd of gecanceld worden. Standaard wordt de Abort Policy gebruikt, deze throw altijd een RejectedExecutionException op met de naam van de taak en de naam van de executor.

Testen

Als laatste behandelen we het testen. Er zijn drie zaken die je met jouw testen wilt borgen als je gebruikt maakt van scheduling met Spring, dat zijn:

  1. De scheduling is goed geconfigureerd voor algemeen gebruik binnen de applicatie
  2. De scheduling is goed geconfigureerd voor de tijdstippen die ik gepland heb
  3. Mijn code in de geannoteerde @Scheduling method body doet wat ik ervan verwacht

Met deze strategie kunnen wij nu kijken hoe we dit bereiken voor de eerste twee punten, het laatste punt is immers een reguliere test zoals de rest van de code binnen de applicatie. Voor het eerste punt kunnen wij de ThreadpoolExecution bean inspecteren op properties die ingesteld zijn zoals bijvoorbeeld de threadpool size. Mooi is dat deze test ook meteen zal falen wanneer de scheduling niet actief is gemaakt in de applicatie. Deze test kun je in de code sample vinden in de klasse ThreadPoolExecutionTest.

Het tweede punt is te testen door de lijst te vinden die Spring bijhoudt van de taken die ingepland worden. Spring houdt deze taken bij in een klasse genaamd ScheduledTaskHolder. In deze klasse is een lijst te benaderen waar alle verwerkte methoden met de annotatie @Scheduled. Deze test kun je in de code sample vinden in de test packages configuration, scheduling en unittest.

In dit artikel zijn de meest gebruikte scheduling delen besproken. Goed om te weten is dat er ook Trigger mogelijkheden zijn. Daarnaast valt er nog meer te configureren wanneer je scheduling gebruikt, te denken valt aan de combinatie met de async opties en het gebruiken van eigen java.concurrent.Executor subklassen.  Ook biedt Spring integratie met de Quartz library. Er zijn meer Cron expressie mogelijkheden dan besproken, deze vind je in de Spring documentatie terug. Denk hierbij aan expressies waarbij je de eerste of laatste van uur, week of maand wilt aangeven of de eerste of laatste werkdag van een maand wilt definiëren. Wil je hier dieper in duiken dan kun je de documentatie raadplegen en is de broncode goed gedocumenteerd door middel van javadoc en testen die Spring zelf heeft. Alle behandelde onderwerpen zijn in een Github repository te vinden die als naslag en playground kan dienen.

Links

https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#scheduling

https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-auto-configuration-classes.html

https://github.com/bendh/spring-scheduling-article

Bio

Ben Ooms is een gepassioneerde Java engineer, Spring fan en architect bij de Kamer van koophandel. Ben doet graag kennis op en deelt deze graag door coaching, schrijven en presentaties geven.