Shedlock : Scheduled tasks execution framework (Spring Boot)
Posted in Recipe on October 13, 2022 by Venkatesh S ‐ 6 min read
ShedLock with Spring Boot
Introduction
Typically in distributed environments, when we deploy applications sometimes we have a requirement that a task executes only once across the distributed systems.
Spring, by default, cannot handle scheduler synchronization over multiple instances. It executes the jobs simultaneously on every single node instead.
We’ll look at ShedLock, a Java library that makes sure our scheduled tasks run only once at the same time and is an alternative to Quartz. ShedLock is designed to be used in situations where you have scheduled tasks that are not ready to be executed in parallel, but can be safely executed repeatedly. Moreover, the locks are time-based and ShedLock assumes that clocks on the nodes are synchronized.
While there are plenty of schedulers which we can use, note that majority of these schedulers still try to execute jobs in parallel across the distributed nodes.
Shedlock is not a full fledge scheduler, its just a lock. ShedLock makes sure that your scheduled tasks are executed at most once at the same time. If a task is being executed on one node, it acquires a lock which prevents execution of the same task from another node (or thread).
ShedLock uses an external store like Mongo, JDBC database, Redis, Hazelcast, ZooKeeper or others for coordination.
Shedlock configuration
Ensure that you have a spring boot project and follow the steps along.
- Adding 2 shedlock dependencies. One shedlock-spring and another shedlock-provider-jdbc-template. Note that shedlock saves the state in the DB and uses it for deciding to either execute a task or skip it.
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>4.42.0</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>4.42.0</version>
</dependency>
- Adding a datasouce. By default you will have a datasource configured to your DB. Incase you are missing it, you can add the configuration in your application.yml or application.properties file. Note this example uses a h2 DB.
spring:
datasource:
driverClassName: org.h2.Driver
url: jdbc:h2:mem:shedlock_DB;INIT=CREATE SCHEMA IF NOT EXISTS shedlock;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
- Also ensure that the following table is created the above schema.
CREATE TABLE shedlock(name VARCHAR(64) NOT NULL, lock_until TIMESTAMP(3) NOT NULL,
locked_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), locked_by VARCHAR(255) NOT NULL, PRIMARY KEY (name));
- Creating a scheduler configuration to enable shedlock and configuring a lock provider. In this example we use JdbcTemplateLockProvider since we are persisting the state in the DB.
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
@Configuration
public class SchedulerConfig {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(dataSource);
}
}
- Creating your scheduled tasks. This shows an example scheduled task that executes on a cron trigger. Note that the time lockAtLeastFor and lockAtMostFor uses the time expressions as per ISO8601 Duration formats. You can add as many scheduled tasks as you wish.
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
@Component
public class MyTaskScheduler {
@Scheduled(cron = "*/10 * * * * *")
@SchedulerLock(name = "MyTaskScheduler", lockAtLeastFor = "PT30S", lockAtMostFor = "PT1M")
public void run() {
System.out.println("My Scheduled Task");
}
}
- Tying everything together using your application main class. We have to provide @EnableScheduling and @EnableSchedulerLock annotations on our Spring configuration class. This will enable shedlock scheduler.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
@SpringBootApplication
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
public class ShedlockApplication {
public static void main(String[] args) {
SpringApplication.run(ShedlockApplication.class, args);
}
}
In normal situations, ShedLock releases the lock directly after the task finishes. However we can decide how long the lock can be held and overide the defaultLockAtMostFor parameter to ensure that the task does not get locked infinitly. Complete source code example available at https://github.com/vensr/shedlock-app
Now you can start your application and run it. You should see that the scheduled task executes every 10 seconds on the console.
mvn spring-boot:run
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------< com.minilab:shedlock >------------------------
[INFO] Building shedlock 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] >>> spring-boot-maven-plugin:2.7.4:run (default-cli) > test-compile @ shedlock >>>
[INFO]
[INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ shedlock ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] Copying 1 resource
[INFO] Copying 1 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.10.1:compile (default-compile) @ shedlock ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-resources-plugin:3.2.0:testResources (default-testResources) @ shedlock ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] skip non existing resourceDirectory /home/venkatesh/Projects/opensource/shedlock-app/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.10.1:testCompile (default-testCompile) @ shedlock ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] <<< spring-boot-maven-plugin:2.7.4:run (default-cli) < test-compile @ shedlock <<<
[INFO]
[INFO]
[INFO] --- spring-boot-maven-plugin:2.7.4:run (default-cli) @ shedlock ---
[INFO] Attaching agents: []
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.4)
2022-10-13 13:50:31.555 INFO 70408 --- [ main] c.minilab.shedlock.ShedlockApplication : Starting ShedlockApplication using Java 17.0.4 on venkatesh-personal with PID 70408 (/home/venkatesh/Projects/opensource/shedlock-app/target/classes started by venkatesh in /home/venkatesh/Projects/opensource/shedlock-app)
2022-10-13 13:50:31.558 INFO 70408 --- [ main] c.minilab.shedlock.ShedlockApplication : No active profile set, falling back to 1 default profile: "default"
2022-10-13 13:50:31.983 INFO 70408 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2022-10-13 13:50:31.998 INFO 70408 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 5 ms. Found 0 JPA repository interfaces.
2022-10-13 13:50:32.420 INFO 70408 --- [ main] o.f.c.internal.license.VersionPrinter : Flyway Community Edition 9.4.0 by Redgate
2022-10-13 13:50:32.420 INFO 70408 --- [ main] o.f.c.internal.license.VersionPrinter : See what's new here: https://flywaydb.org/documentation/learnmore/releaseNotes#9.4.0
2022-10-13 13:50:32.420 INFO 70408 --- [ main] o.f.c.internal.license.VersionPrinter :
2022-10-13 13:50:32.431 INFO 70408 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2022-10-13 13:50:32.662 INFO 70408 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2022-10-13 13:50:32.679 INFO 70408 --- [ main] o.f.c.i.database.base.BaseDatabaseType : Database: jdbc:h2:mem:shedlock_DB (H2 2.1)
2022-10-13 13:50:32.791 INFO 70408 --- [ main] o.f.core.internal.command.DbValidate : Successfully validated 1 migration (execution time 00:00.009s)
2022-10-13 13:50:32.796 INFO 70408 --- [ main] o.f.c.i.s.JdbcTableSchemaHistory : Creating Schema History table "PUBLIC"."flyway_schema_history" ...
2022-10-13 13:50:32.887 INFO 70408 --- [ main] o.f.core.internal.command.DbMigrate : Current version of schema "PUBLIC": << Empty Schema >>
2022-10-13 13:50:32.899 INFO 70408 --- [ main] o.f.core.internal.command.DbMigrate : Migrating schema "PUBLIC" to version "1 - shedlock"
2022-10-13 13:50:32.914 INFO 70408 --- [ main] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema "PUBLIC", now at version v1 (execution time 00:00.041s)
2022-10-13 13:50:33.090 INFO 70408 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2022-10-13 13:50:33.164 INFO 70408 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 5.6.11.Final
2022-10-13 13:50:33.312 INFO 70408 --- [ main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2022-10-13 13:50:33.455 INFO 70408 --- [ main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2022-10-13 13:50:33.610 INFO 70408 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2022-10-13 13:50:33.618 INFO 70408 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2022-10-13 13:50:33.795 INFO 70408 --- [ main] c.minilab.shedlock.ShedlockApplication : Started ShedlockApplication in 3.071 seconds (JVM running for 3.724)
My Scheduled Task