A Lightweight Job Scheduling Framework for Java.
Sundial makes adding scheduled jobs to your Java application a walk in the park. Simply define jobs, define triggers, and start the Sundial scheduler.
Sundial is a lightweight Java job scheduling framework forked from Quartz (http://www.quartz-scheduler.org/) stripped down to the bare essentials. Sundial also hides the nitty-gritty configuration details of Quartz, reducing the time needed to get a simple RAM job scheduler up and running. Sundial uses a ThreadLocal wrapper for each job containing a HashMap for job key-value pairs. Convenience methods allow easy access to these parameters. JobActions are reusable components that also have access to the context parameters. If you are looking for a hassle-free 100% Java job scheduling framework that is easy to integrate into your applications, look no further.
- Apache 2.0 license
- ~150 KB Jar
- In-memory multi-threaded jobs
- Define jobs and triggers in jobs.xml
- or define jobs and triggers via annotations
- or define jobs and triggers programmatically
- Cron Triggers
- Simple Triggers
- Manual Triggers (register jobs with no automatic trigger, run on demand)
- Java 17 and up
- Depends only on slf4j
public class SampleJob extends org.knowm.sundial.Job {
@Override
public void doRun() throws JobInterruptException {
// Do something interesting...
}
}@CronTrigger(cron = "0/5 * * * * ?")@SimpleTrigger(repeatInterval = 30, timeUnit = TimeUnit.SECONDS)Use @ManualTrigger to register a job with the scheduler on startup without any automatic trigger. The job will only run when explicitly started via SundialJobScheduler.startJob() or the admin task endpoint.
@ManualTrigger
public class SampleJob extends org.knowm.sundial.Job {
@Override
public void doRun() throws JobInterruptException {
// Do something interesting...
}
}Optionally allow concurrent execution or provide a job data map:
@ManualTrigger(isConcurrencyAllowed = true, jobDataMap = { "KEY_1:VALUE_1", "KEY_2:1000" })
public class SampleJob extends org.knowm.sundial.Job { ... }public static void main(String[] args) {
SundialJobScheduler.startScheduler("org.knowm.sundial.jobs"); // package with annotated Jobs
}If you need a bigger thread pool (default size is 10) use startScheduler(int threadPoolSize, String annotatedJobsPackageName) instead.
If you want full control over the thread pool (e.g. to share an existing executor, set custom thread factories, or use a virtual-thread executor), you can pass your own ExecutorService directly:
ExecutorService executor = Executors.newFixedThreadPool(20);
SundialJobScheduler.startScheduler(executor, "org.knowm.sundial.jobs");The caller is responsible for the executor's lifecycle — Sundial will not shut it down.
By default Sundial creates job instances via plain reflection (new MyJob()). This means fields annotated with @Inject or @Autowired are not populated — the DI container never sees the object.
Dependency Injection is a pattern where the framework (Guice, Spring, etc.) is responsible for constructing objects and wiring their dependencies. For scheduled jobs this matters when a job needs a service, repository, or any other managed bean injected into it.
You can plug in any DI container by supplying a custom JobFactory after the scheduler starts:
// Guice example
Injector injector = Guice.createInjector(new MyAppModule());
SundialJobScheduler.startScheduler("org.knowm.sundial.jobs");
SundialJobScheduler.setJobFactory((bundle, scheduler) ->
injector.getInstance(bundle.getJobDetail().getJobClass()));// Spring example
@Bean
public CommandLineRunner schedulerStarter(ApplicationContext ctx) {
return args -> {
SundialJobScheduler.startScheduler("org.knowm.sundial.jobs");
SundialJobScheduler.setJobFactory((bundle, scheduler) ->
(Job) ctx.getBean(bundle.getJobDetail().getJobClass()));
};
}Each time a trigger fires, Sundial calls your factory to produce a fresh job instance, so the DI container can inject a new set of dependencies per execution.
<?xml version='1.0' encoding='utf-8'?>
<job-scheduling-data>
<schedule>
<!-- job with cron trigger -->
<job>
<name>SampleJob3</name>
<job-class>com.foo.bar.jobs.SampleJob3</job-class>
<concurrency-allowed>true</concurrency-allowed>
</job>
<trigger>
<cron>
<name>SampleJob3-Trigger</name>
<job-name>SampleJob3</job-name>
<cron-expression>*/15 * * * * ?</cron-expression>
</cron>
</trigger>
<!-- job with simple trigger -->
<job>
<name>SampleJob2</name>
<job-class>com.foo.bar.jobs.SampleJob2</job-class>
<job-data-map>
<entry>
<key>MyParam</key>
<value>42</value>
</entry>
</job-data-map>
</job>
<trigger>
<simple>
<name>SampleJob2-Trigger</name>
<job-name>SampleJob2</job-name>
<repeat-count>5</repeat-count>
<repeat-interval>5000</repeat-interval>
</simple>
</trigger>
</schedule>
</job-scheduling-data>SundialJobScheduler.addJob("SampleJob", "org.knowm.sundial.jobs.SampleJob");
SundialJobScheduler.addCronTrigger("SampleJob-Cron-Trigger", "SampleJob", "0/10 * * * * ?");
SundialJobScheduler.addSimpleTrigger("SampleJob-Simple-Trigger", "SampleJob", -1, TimeUnit.SECONDS.toMillis(3));// asynchronously start a job by name
SundialJobScheduler.startJob("SampleJob");
// interrupt a running job
SundialJobScheduler.stopJob("SampleJob");
// remove a job from the scheduler
SundialJobScheduler.removeJob("SampleJob");
// remove a trigger from the scheduler
SundialJobScheduler.removeTrigger("SampleJob-Trigger");
// lock scheduler
SundialJobScheduler.lockScheduler();
// unlock scheduler
SundialJobScheduler.unlockScheduler();
// check if job a running
SundialJobScheduler.isJobRunning("SampleJob");And many more useful functions. See all here: https://github.com/knowm/Sundial/blob/develop/src/main/java/org/knowm/sundial/SundialJobScheduler.java
// asynchronously start a job by name with data map
Map<String, Object> params = new HashMap<>();
params.put("MY_KEY", new Integer(660));
SundialJobScheduler.startJob("SampleJob1", params);// annotate CronTrigger with data map (separate key/values with ":" )
@CronTrigger(cron = "0/5 * * * * ?", jobDataMap = { "KEY_1:VALUE_1", "KEY_2:1000" })
public class SampleJob extends Job {
}<!-- configure data map in jobs.xml -->
<job>
<name>SampleJob</name>
<job-class>org.knowm.sundial.jobs.SampleJob</job-class>
<job-data-map>
<entry>
<key>MyParam</key>
<value>42</value>
</entry>
</job-data-map>
</job>// access data inside Job
String value1 = getJobContext().get("KEY_1");
logger.info("value1 = " + value1);With JobActions, you can encapsule logic that can be shared by different Jobs.
public class SampleJobAction extends JobAction {
@Override
public void doRun() {
Integer myValue = getJobContext().get("MyValue");
// Do something interesting...
}
}// Call the JobAction from inside a Job
getJobContext().put("MyValue", new Integer(123));
new SampleJobAction().run();To terminate a Job asynchronously, you can call the SundialJobScheduler.stopJob(String jobName) method. Sundial provides two mechanisms for stopping a running job:
This is the default approach. It works by setting a flag that the job should stop; the job is responsible for polling that flag. In any long-running job that you anticipate the need to terminate, call checkTerminated() at an appropriate location (e.g. inside a loop).
For an example see SampleJob9.java. In a loop within the Job you should just add a call to checkTerminated();.
If you try to shutdown the SundialScheduler and it just hangs, it's probably because you have a Job defined with an infinite loop with no checkTerminated(); call. You may see a log message like: Waiting for Job to shutdown: SampleJob9 : SampleJob9-trigger.
A job can also stop itself early by throwing JobInterruptException from within doRun(). This is useful when the job detects a condition that means it should not continue (e.g. nothing to process, a prerequisite failed, a count threshold was reached).
public class HelloJob extends Job {
private int count = 0;
@Override
public void doRun() throws JobInterruptException {
System.out.println("Hello Job #" + (++count));
if (count >= 3) {
// Stop this execution early. The trigger will still fire again on schedule.
throw new JobInterruptException();
}
}
}Throwing JobInterruptException stops the current execution of the job cleanly — the finally block (and cleanup()) still runs. It does not remove the trigger or prevent future executions. To also stop future executions, call SundialJobScheduler.stopJob(jobName) or SundialJobScheduler.removeTrigger(triggerName) before throwing.
The interrupted execution is logged at DEBUG level as: Job [HelloJob] interrupted.
If your job blocks on I/O or other interruptible operations (e.g. Thread.sleep(), InputStream.read(), CountDownLatch.await()), it may never reach a checkTerminated() call. In that case, extend InterruptingJob instead of Job:
public class SampleJob10 extends InterruptingJob {
@Override
public void doRun() throws JobInterruptException {
try {
Thread.sleep(60_000); // or any blocking I/O
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // preserve interrupt status
logger.info("Job was interrupted.");
}
}
}When SundialJobScheduler.stopJob("SampleJob10") is called, InterruptingJob will call Thread.interrupt() on the executing thread, immediately unblocking it from the blocking call. You must handle InterruptedException and re-interrupt the thread so the interrupt status is preserved.
See SampleJob10.java for a full example.
By default jobs are not set to concurrently execute. This means if a job is currently running and a trigger is fired for that job, it will skip running the job. In some cases concurrent job execution is desired and there are a few ways to configure it.
- You can add
<concurrency-allowed>true</concurrency-allowed>in jobs.xml. - You can add it to the Sundial annotations like this:
@SimpleTrigger(repeatInterval = 30, timeUnit = TimeUnit.SECONDS, isConcurrencyAllowed = true)Same idea for cron annotation too.
Now go ahead and study some more examples, download the thing and provide feedback.
Download Jar: http://knowm.org/open-source/sundial/sundial-change-log/
- org.slf4j.slf4j-api-2.0.12
The Sundial release artifacts are hosted on Maven Central.
Add the Sundial library as a dependency to your pom.xml file:
<dependency>
<groupId>org.knowm</groupId>
<artifactId>sundial</artifactId>
<version>2.4.0</version>
</dependency>For snapshots, add the following to your pom.xml file:
<repository>
<id>central-portal-snapshots</id>
<snapshots/>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
</repository>
<dependency>
<groupId>org.knowm</groupId>
<artifactId>sundial</artifactId>
<version>2.5.0-SNAPSHOT</version>
</dependency>mvn clean package
mvn javadoc:javadoc
mvn com.spotify.fmt:fmt-maven-plugin:format
mvn versions:display-dependency-updates
See the Cron Trigger tutorial over at quartz-scheduler.org. Here are a few examples:
| Expression | Meaning |
|---|---|
| 0 0 12 * * ? | Fire at 12pm (noon) every day |
| 0 15 10 * * ? | Fire at 10:15am every day |
| 0 15 10 ? * MON-FRI | Fire at 10:15am every Monday, Tuesday, Wednesday, Thursday and Friday |
| 0 0/10 * * * ? | Fire every 10 mintes starting at 12 am (midnight) every day |
Please report any bugs or submit feature requests to Sundial's Github issue tracker.
