Wiki

Clone wiki

scheduler / ReferenceManual

Introduction

TimeMachine is an open source Java scheduler that can run high volume of jobs with many different type of schedules. It supports repeating schedule on fixed interval, CRON based expression, or even custom schedules. The scheduler can manage job executions with thread pools, and it can persist job data into different storage. Users may run the scheduler as stand-alone server, or deploy as web application. The scheduler engine is using a stack-based service container that is easy to configure and extend by developers.

Main Features:

  • A stand-alone scheduler server with simple Properties configuration file.
  • A web application (war) to run and manage the scheduler.
  • Support job implementation in either Java or scripting language (Eg: Groovy)
  • Support multiple thread pools for isolated jobs execution.
  • Support in-memory or database data store.
  • Support scheduler clustering - multiple scheduler nodes with single logical scheduler.
  • Support job history recording.
  • Built-in JobTask: LoggerJobTask, ScriptingJobTask and OsCommandJobTask.
  • Built-in Schedule: RepeatSchedule, CronSchedule and DateListSchedule.
  • Built-in Service: Crontab that's similar to Linux/Unix OS crontab service.
  • Built-in Service: JobLoader to load any job and schedule with simple properties file.

This is a reference manual for the TimeMachine Scheduler. It provides user and developer guides to use and understand the scheduler functionalities. The User Guide is intended to be used by end users or administrators who need to monitor, run and a maintains a scheduler server instances. The Developer Guide is intended for developers who need to write custom jobs, or custom user services to extend the scheduler functionalities.

Although the scheduler is written in Java, but users and developers have the options to run any job using native, external OS commands or scripts; or to write custom job task in either Java or any dynamic programming languages (eg: JavaScript, Groovy etc) that supported by JVM platform.

Design Principle and Goals

We want to provide a job scheduler that can handle high volumes of tasks and use flexible schedules. User should able to configure and use it out of the box, and yet developers can still customize to do exactly what they want. Therefore TimeMachine scheduler is both a scheduler server and a library for developers to write rich and complex jobs and run them in a highly scalable, enterprise environment.

The core concept of TimeMachine is we will make its components stackable. The smallest component that does a logical unit of work is called a Service. A Service will have a well-defined life cycle as the application initializes, starts, stops and is destroyed. The entire scheduler application is just a container of many system Services such as MemoryDataStore, FixedSizeThreadPool, and SchedulerEngine, etc. Not only this, but we will also allow user to extend the system by implementing the same simple Service interface!

For scheduling and tasking, we will try to keep the API as minimal as possible, and yet remain flexible and extendable. For example, we will try to make all Service and scheduler classes as close to POJOs as possible. This helps user to clearly see the method names in use, and it can be used by other fancier IoC if necessary. We will try hard to balance out what we think will be the most frequently used task and schedules, and the most frequently used classes, under the main 'timemachine.scheduler' package, and then move all other supporting implementations elsewhere in packages, so users will see less clutter when using the API.

Support

This is an open source community supported project. Please see our UserGroup for general support.

Issues

https://bitbucket.org/timemachine/scheduler/issues

License

The TimeMachine Scheduler source and its binary are under a open source friendly Apache License 2.0.

Our download package also contains several optional open source libraries such as Groovy, Log4j, and H2database etc. These are not required by scheduler directly, but we include them for practical and convenience purpose. Please ensure you read their licenses before use. You may remove these if they don't fit your need.

User Guide: Running Scheduler and Setup Configuration

Getting Started

We have two full distribution files you may download, and you only need to pick one to run the scheduler.

Installing the zip package

Choose this package if you want to run scheduler as command line. The zip package also contains additional config samples that you get you started quickly.

  1. Check to see if you have JRE 6 or higher on your system.
  2. Download the TimeMachine zip file binary package from https://bitbucket.org/timemachine/scheduler/downloads
  3. Unzip package in your local file system.

Installing the war package

Choose this package if you want to run scheduler as web application. It requires you to have a servlet container installed first. More instruction on how to install and configure the web application can be found in WebConsole.

The web application will automatically starts an instance of scheduler upon deployment. There is a web UI with form that allow you to view jobs, edit the config, or even manage the scheduler using Groovy scripting console.

In the UI, there is a "Config" screen that you may change and reload the scheduler configuration settings.

Running the scheduler

In the rest of this reference documentation, we assume you've downloaded the zip package. So follow along.

TimeMachine is easy to use, and it comes with a server that you run without writing any code. Try open a command prompt and cd into the unzipped project directory that contains the bin folder, and then run:

$ bin/scheduler.sh

From the console logging output, you should see a scheduler initialized and started. Without passing a configuration file argument to the scheduler, the started scheduler will print couple of log messages and then just sit there; doing nothing. You may hit CTRL+C to stop it. You should see the scheduler log output for stopping and destroying the process. The prompt should return to you in a brief moment. There is your first run of the scheduler!

The above script should work on most of the Linux/Unix/MacOSX/Cygwin command prompt shell. If you're running on Windows OS, you may try the bat file like this instead:

C:> bin\scheduler.bat

Here is an example of a scheduler output:

$ bin/scheduler.sh
15:18:14 main INFO| TimeScheduler system services initialized: [
  scheduler: SchedulerData[id=1, name=TimeMachineScheduler],
  schedulerNode: SchedulerNode[nodeId=1, name=TimeMachineScheduler, ip=10.247.2.56],
  configProps: DEFAULT,
  dataStore: MemoryDataStore[name=25199001],
  scheduleRunner: PollingScheduleRunner[name=25094328],
  classLoader: SimpleClassLoaderService[name=18680399],
  jobTaskFactory: SimpleJobTaskFactory[name=1760304],
  jobTaskPoolNameResolver: SimpleJobTaskPoolNameResolver[name=25589390],
  jobTaskThreadPool: DynamicThreadPool[name=jobTaskThreadPool.DEFAULT],
]
15:18:14 main INFO| Scheduler[id=1, nodeId=1, nodeIp=10.247.2.56] initialized. Version=1.0.0-SNAPSHOT.052820121339
15:18:14 main INFO| Scheduler[id=1, nodeId=1, nodeIp=10.247.2.56] started.
... [CTRL+C]
15:18:18 Thread-1 INFO| Scheduler[id=1, nodeId=1, nodeIp=10.247.2.56] stopped.
15:18:18 Thread-1 INFO| Scheduler[id=1, nodeId=1, nodeIp=10.247.2.56] destroyed.

Tips: if the start up script did not work on your environment, then you may try to run the full java command below to start the scheduler. Again assume you have cd into the unzipped directory that contains bin, config and lib directories.

# For Linux/Unix/MacOSX
$ java -cp "config:lib/*" timemachine.scheduler.tool.SchedulerServer

# For Windows/Cygwin
C:> java -cp "config;lib\*" timemachine.scheduler.tool.SchedulerServer

You may also pass --help the command to see more options available.

Next you will see how to configure the scheduler with a properties file to load user jobs and schedules.

Running Crontab service

The TimeMachine scheduler comes with a user service that acts just like a Unix CRONTAB system! All you need to do is create a config properties text file, (eg: name it crontab.properties), that looks like this:

timemachine.scheduler.userservice.crontab.class = timemachine.scheduler.userservice.CrontabService
CrontabService.01 = 0 0 * * * ?        | sh -c echo "Hourly task begins."
CrontabService.02 = 0/5 * * * * ?      | sh -c echo "Heart beat."
CrontabService.03 = 0 0/5 * * * ?      | sh -c echo "Five minutes job."
CrontabService.04 = 0 0 12 * JAN,JUN ? | sh -c echo "We should clean up every 6 months."
CrontabService.05 = 0 0 8 ? * 1-5      | sh -c echo "Every workday at 8AM."

Now run it by the scheduler again with the config file as argument:

$ bin/scheduler.sh config/crontab.properties

If you are on Windows, try using the crontab-windows.properties file instead. The configuration is calling native commands that is OS specific, such as using cmd.exe to call out shell commands etc.

Now inspect the console logging output, and you can verify that your jobs are indeed running! You should see output similar to this:

C:> bin\scheduler.bat config\crontab-windows.properties
15:20:07 main INFO| TimeScheduler system services initialized: [
  scheduler: SchedulerData[id=1, name=TimeMachineScheduler],
  schedulerNode: SchedulerNode[nodeId=1, name=TimeMachineScheduler, ip=10.247.2.56],
  configProps: config/crontab-windows.properties,
  dataStore: MemoryDataStore[name=10774273],
  scheduleRunner: PollingScheduleRunner[name=17548445],
  classLoader: SimpleClassLoaderService[name=17934197],
  jobTaskFactory: SimpleJobTaskFactory[name=13120210],
  jobTaskPoolNameResolver: SimpleJobTaskPoolNameResolver[name=21662929],
  jobTaskThreadPool: DynamicThreadPool[name=jobTaskThreadPool.DEFAULT],
]
15:20:07 main INFO| Scheduled JobDef[id=1, name=01] with CronSchedule[id=1, nextRun=05/28/2012 16:00:00].
15:20:07 main INFO| Scheduled JobDef[id=2, name=02] with CronSchedule[id=2, nextRun=05/28/2012 15:20:10].
15:20:07 main INFO| Scheduled JobDef[id=3, name=03] with CronSchedule[id=3, nextRun=05/28/2012 15:25:00].
15:20:07 main INFO| Scheduled JobDef[id=4, name=04] with CronSchedule[id=4, nextRun=06/29/2012 12:00:00].
15:20:07 main INFO| Scheduled JobDef[id=5, name=05] with CronSchedule[id=5, nextRun=05/29/2012 08:00:00].
15:20:07 main INFO| Scheduler[id=1, nodeId=1, nodeIp=10.247.2.56] initialized. Version=1.0.0-SNAPSHOT.052820121339
15:20:07 main INFO| Scheduler[id=1, nodeId=1, nodeIp=10.247.2.56] started.
15:20:10 TimeMachineScheduler-JobTask-Thread-1 DEBUG| Running DefaultJobContext[jobDef.id=2, schedule.id=2, scheduler.id=1]
15:20:10 TimeMachineScheduler-JobTask-Thread-1 INFO| Executing OS command: [cmd, /c, echo, Heart beat.]
15:20:10 Thread-2 DEBUG| CommandOutput: "Heart beat."
15:20:10 TimeMachineScheduler-JobTask-Thread-1 DEBUG| Command finished with exitCode=0
15:20:10 TimeMachineScheduler-JobTask-Thread-1 DEBUG| DefaultJobContext[jobDef.id=2, schedule.id=2, scheduler.id=1] has been executed.
15:20:15 TimeMachineScheduler-JobTask-Thread-1 DEBUG| Running DefaultJobContext[jobDef.id=2, schedule.id=2, scheduler.id=1]
15:20:15 TimeMachineScheduler-JobTask-Thread-1 INFO| Executing OS command: [cmd, /c, echo, Heart beat.]
15:20:15 Thread-3 DEBUG| CommandOutput: "Heart beat."
15:20:15 TimeMachineScheduler-JobTask-Thread-1 DEBUG| Command finished with exitCode=0
15:20:15 TimeMachineScheduler-JobTask-Thread-1 DEBUG| DefaultJobContext[jobDef.id=2, schedule.id=2, scheduler.id=1] has been executed.
15:20:20 TimeMachineScheduler-JobTask-Thread-1 DEBUG| Running DefaultJobContext[jobDef.id=2, schedule.id=2, scheduler.id=1]
15:20:20 TimeMachineScheduler-JobTask-Thread-1 INFO| Executing OS command: [cmd, /c, echo, Heart beat.]
15:20:20 Thread-4 DEBUG| CommandOutput: "Heart beat."
15:20:20 TimeMachineScheduler-JobTask-Thread-1 DEBUG| Command finished with exitCode=0
15:20:20 TimeMachineScheduler-JobTask-Thread-1 DEBUG| DefaultJobContext[jobDef.id=2, schedule.id=2, scheduler.id=1] has been executed.
15:20:25 TimeMachineScheduler-JobTask-Thread-1 DEBUG| Running DefaultJobContext[jobDef.id=2, schedule.id=2, scheduler.id=1]
15:20:25 TimeMachineScheduler-JobTask-Thread-1 INFO| Executing OS command: [cmd, /c, echo, Heart beat.]
15:20:25 Thread-5 DEBUG| CommandOutput: "Heart beat."
15:20:25 TimeMachineScheduler-JobTask-Thread-1 DEBUG| Command finished with exitCode=0
15:20:25 TimeMachineScheduler-JobTask-Thread-1 DEBUG| DefaultJobContext[jobDef.id=2, schedule.id=2, scheduler.id=1] has been executed.
15:20:30 TimeMachineScheduler-JobTask-Thread-1 DEBUG| Running DefaultJobContext[jobDef.id=2, schedule.id=2, scheduler.id=1]
15:20:30 TimeMachineScheduler-JobTask-Thread-1 INFO| Executing OS command: [cmd, /c, echo, Heart beat.]
15:20:30 Thread-6 DEBUG| CommandOutput: "Heart beat."
15:20:30 TimeMachineScheduler-JobTask-Thread-1 DEBUG| Command finished with exitCode=0
15:20:30 TimeMachineScheduler-JobTask-Thread-1 DEBUG| DefaultJobContext[jobDef.id=2, schedule.id=2, scheduler.id=1] has been executed.
...

The format to the crontab service config file is described here:

timemachine.scheduler.userservice.crontab.class = timemachine.scheduler.userservice.CrontabService
CrontabService.<uniqueJobName> = <CronExpression> | <os_command_and_arguments>

You declare the crontab service to TimeMachine (first line), and then add any number of cron entries with the JobLoaderService. prefix. You want to ensure each entry has <uniqueJobName> on left, and then a CRON expression and a OS command on the right. A | separator is used to separate the CRON expression and the OS command. You may invoke any OS commands that is available in your system $PATH. For more details on CRON expression format and usage, see CronSchedule API.

Running JobLoader service

The crontab service above would only allow you to create cron schedule to run a OS native command as a job. But TimeMachine scheduler is a Java based application, so you may write your own JobTask that perform any work you want. The scheduler also comes with a user service called JobLoaderService that has more flexible configuration setup to let you configure any provided or custom JobTask job and run with any built-in Schedule.

Here is an example on how to use the JobLoader service. Again you would use a config properties text file (eg: named job-loader.properties):

timemachine.scheduler.userservice.jobLoader.class = timemachine.scheduler.userservice.JobLoaderService
JobLoaderService.01myJob.timemachine.scheduler.jobtask.LoggerJobTask = RepeatSchedule{interval=2|intervalUnit=SECOND|endCount=2}
JobLoaderService.02myJob.timemachine.scheduler.jobtask.LoggerJobTask = RepeatSchedule{interval=1|intervalUnit=MINUTE|startTime=23:15:00}
JobLoaderService.03myJob.timemachine.scheduler.jobtask.LoggerJobTask = JobProps{msg=hello|test=foo}; RepeatSchedule{interval=17|intervalUnit=SECOND|endTime=1/1/2099 00:00:00}
JobLoaderService.04myJob.timemachine.scheduler.jobtask.LoggerJobTask = CronSchedule{expression=* * * * * ?}
JobLoaderService.05myJob.timemachine.scheduler.jobtask.LoggerJobTask = CronSchedule{expression=0 * * * * ?}
JobLoaderService.06myJob.timemachine.scheduler.jobtask.LoggerJobTask = CronSchedule{expression=0 0 8 ? * MON,FRI}
JobLoaderService.07myJob.timemachine.scheduler.jobtask.LoggerJobTask = CronSchedule{expression=0,15,30,45 * * * * ? | endCount=5}
JobLoaderService.08myJob.timemachine.scheduler.jobtask.SleepyJobTask = DateListSchedule{datetime.1=1/1/2012 00:00:00}
JobLoaderService.09myJob.timemachine.scheduler.jobtask.LoggerJobTask = DateListSchedule{datetime.1=1/1/2012 00:00:00|datetime.2=2/1/2012 23:59:59}
JobLoaderService.10myJob.timemachine.scheduler.jobtask.ScriptingJobTask = JobProps{scriptEgnineName=JavaScript|scriptText=1+1}; RepeatSchedule{endCount=1}
JobLoaderService.11myJob.timemachine.scheduler.jobtask.OsCommandJobTask = JobProps{commandLine=echo "Hello World!"}; RepeatSchedule{endCount=1}

Again, you may fire up the scheduler with above config file to see it in action:

$ bin/scheduler.sh config/job-loader.properties

As you can see in the config file, I configured to load numerous jobs upon the scheduler starts, and it will run according to different schedules specified. The format of the config file is follow:

timemachine.scheduler.userservice.jobLoader.class = timemachine.scheduler.userservice.JobLoaderService
JobLoaderService.<uniqueJobName>.<jobTaskClass> = [JobProps{key=value|key2=value2 ...};] <scheduleType>{key=value|key2=value2 ...}

First line is declaration of the JobLoader serivce, and then you may add any entry following that with the JobLoaderService. prefix. You would need to ensure <uniqueJobName> is unique per key entry.

The <jobTaskClass> is the Java's full package class name to one of JobTask implementation. This is the actual class/code that does the work of your job. You may choose and use any of the following or write your own.

  • LoggerJobTask - A job that just log the content of the job and schedule information.
  • OsCommandJobTask - A job to run any native OS command.
  • ScriptingJobTask - A job to execute Scripting text or file.

Each line, or job definition may an optional job properties map you may configure and control it through a JobProps. With the ScriptingJobTask for example, you can configure its 'commandLine' to execute any native command.

After that, you still need a <scheduleType> to tell how often you want the JobTask to be run. You may choose one of these to use:

  • RepeatSchedule - run job using an fixed repeating, and it supports interval and intervalUnit properties.
  • CronSchedule - run job using an Unix system CRON with expression property. See CronSchedule API for more on the format usage.
  • DeteListSchedule - run job using an explicit list of dates. Use list of datetime.<#> properties.

Right after the <scheduleType>, you may enter a map of the properties within the curly brackets in key=value format. We use | to delimiter each pair. Below are few common properties that you may use in addition to ones mentioned above.

  • startTime - Set a start time for when the schedule starts to run. Enter datetime in MM/dd/yyyy HH:mm:ss format.
  • startTimeDelay - Set a start time with this number of millis of seconds delay from now.
  • endTime - Set an end time when the last run for the schedule. Enter datetime in MM/dd/yyyy HH:mm:ss format.
  • endCount - Set an end count when the last run for the schedule.
  • priority - Set a priority value 1-9 on Schedule. When two or more Schedules that has same nextRun datetime, the one with lower value (1) will have a higher priority to run first. Default value is 5.

How Scheduler store and run Jobs with Schedules

In TimeMachine, you can store any number of jobs, or more precisely job definitions. The scheduler should able to scale up and run thousands of jobs without issues on a typical modern workstation. The scheduler is highly configurable also, so that you may choose your optimal settings for the jobs to run under specific hardware available.

Understanding how the scheduler organize and run jobs would help you better configure the system. The following are the key classes and terms in understanding the TimeMachine Scheduler:

  • timemachine.scheduler.JobTask This is the actual task or work that will be perform when a job is run. You provide an implementation class name and store into a job definition. The scheduler will instantiate the object on your behalf. You will not create the instance on your own.
  • timemachine.scheduler.Schedule A schedule would tell scheduler when and how often to run the JobTask above. A job definition may have zero or more schedule associate with it. Each schedule represent an "job instance" to be run. We provide few built-in schedule implementation already such as: CronSchedule, RepeateSchedule, or DateListSchedule.
  • timemachine.scheduler.JobDef An instance of this class represents a job definition. The scheduler would only allow you to store or schedule jobs through using a defintion. If you associate any Schedule with it a job definition, then it will become the "job instance" to be run according to the next run time in the schedule.
  • timemachine.scheduler.Scheduler This is the main entry point of the scheduler API. You use to to schedule or unschedule JobDef.

Here is how the scheduler work in general. When you call Scheduler.schedule(jobDef), it will store the job definition into the DataStore service.

A Schedule that associated with a JobDef will provide a nextRun date to tell when it's time to run the job. The Schedule would initially created in WAITING state. When the time is ready, the ScheduleRunner will move the Schedule into a STAGINIG state. After scheduler submitted the Schedule's JobTask to the thread pool, it will then move the Schedule state into RUNNING. Once that JobTask is completed, it would move Schedule state back to WAITING, and calculate update Schedule for next run date.

After a default scheduler has fully started, it will have two ThreadPool services that will perform actual work. There are two types of work that will run in each the ThreadPool:

  • Service - any system or user service that implements java.lang.Runnable. A typical service would not block a thread when calling init() or start(), but if you do have the need for long running service, then you may implements java.lang.Runnable. When you do this, it will be run in this thread pool. For instance, our ScheduleRunner is such an example. The default pool size for service is only set to 1.
  • JobTask - all of the scheduler's job task are run in this thread pool. You can set the pool size higher if your hardware have enough resources and you have large amount of jobs to run. Default thread pool size is set to 4.

Note1: The ScheduleRunner is a system service in the scheduler that responsible to check the DataStore for any Schedule that needs to be run. We provide a PollingScheduleRunner that can check the data store in a fixed interval bases. The PollingScheduleRunner will be smart enough not to wait if there data store has more schedule waiting to be run between two polling cycles.

Note2: You can actually creates more than one thread pools to multiple job that matches to their names! See thread pool configuration below for more details.

JobDef, Schedule, Job Runs and Concurrency

The scheduler allows you to run jobs in a high concurrency mode. This has great affect on how you configure and schedule jobs in the scheduler. Remember that an instance of a JobDef and a Schedule would represent a single job run, and the same JobDef instance could have multiple Schedules (multiple jobs run in this case)! During a Schedule run, a new instance of JobTask from JobDef would be created by scheduler. So the scheduler might have multiple jobs that's from the same JobDef, but it will all have different instance of JobTask and Schedule. The same instance of Schedule of job run will never be running by two threads at the same time in scheduler pool.

One very important thing to remember is that a JobDef may have map of string properties, and during a job run, it can be access by the JobTask. So in a case of one JobDef with multiple Schedules, the string map is not thread safe to perform update! To avoid this, you would want to create one JobDef per Schedule that you want to update. It's best to insert and setup JobProps when JobDef is created, and when job is run from JobTask, you would only perform read access from these string map. If you really want many jobs during runtime to share and update a common data properties, you should store and manage these properties outside of the scheduler storage and related to it by the unique job id.

Scheduler Configuration Properties

The SchedulerServer above will take an Properties file as argument that you may use to control the scheduler. The Scheduler will start up with certain default settings values already. You will find the latest default values in the source file src/main/resources/timemachine/scheduler/default.properties.

Main Scheduler Config Properties

These are properties use to control and setup the Scheduler.

# Logical Scheduler and Clustering Nodes
timemachine.scheduler.schedulerName = TimeMachineScheduler
timemachine.scheduler.nodeName = ${timemachine.scheduler.schedulerName}

# System core service implementations
timemachine.scheduler.dataStore.class = timemachine.scheduler.service.MemoryDataStore
#timemachine.scheduler.dataStore.memoryDataStore.useSingleton = false
timemachine.scheduler.classLoader.class = timemachine.scheduler.service.SimpleClassLoaderService
timemachine.scheduler.jobTaskFactory.class = timemachine.scheduler.service.SimpleJobTaskFactory
timemachine.scheduler.scheduleRunner.class = timemachine.scheduler.service.PollingScheduleRunner
timemachine.scheduler.scheduleRunner.pollingInterval = 1000
timemachine.scheduler.scheduleRunner.maxSchedulesPerInterval = -1
timemachine.scheduler.jobTaskPoolNameResolver.class = timemachine.scheduler.service.SimpleJobTaskPoolNameResolver

# System service thread pool (you only need one pool!)
timemachine.scheduler.systemThreadPool.class = timemachine.scheduler.service.FixedSizeThreadPool
timemachine.scheduler.systemThreadPool.maxSize = 2
timemachine.scheduler.systemThreadPool.threadNamePrefix = ${timemachine.scheduler.schedulerName}-System-Thread-

# Default jobTask thread pool (you may define more than one pool!)
timemachine.scheduler.jobTaskThreadPool.DEFAULT.class = timemachine.scheduler.service.DynamicThreadPool
timemachine.scheduler.jobTaskThreadPool.DEFAULT.minSize = 0
timemachine.scheduler.jobTaskThreadPool.DEFAULT.maxSize = 4
timemachine.scheduler.jobTaskThreadPool.DEFAULT.timeToLive = 300000
timemachine.scheduler.jobTaskThreadPool.DEFAULT.useShutdownNow = false
timemachine.scheduler.jobTaskThreadPool.DEFAULT.maxShutdownWaitTime = 1000
timemachine.scheduler.jobTaskThreadPool.DEFAULT.threadNamePrefix = ${timemachine.scheduler.schedulerName}-JobTask-Thread-

# Resolving multiple jobTask thread pools
#timemachine.scheduler.jobTaskPoolNameResolver.poolName.MY_POOL.matchToJobNameRexp = MyJob.*

# User services
# timemachine.scheduler.userservice.<serviceName>.class = <yourServiceClass>
# timemachine.scheduler.userservice.01MyFirst.class = timemachine.scheduler.example.MyFirstUserService

Many of these properties are self explanatory by its key name. Here we will try to describe them per group of related features. Also note that we allow you to mix any of the user service such as the CrontabService into the main configuration file as well!

Logical Scheduler and Nodes Config Props

  • timemachine.scheduler.schedulerName - A unique name given to a logical scheduler instance. A logical scheduler may contains one or more scheduler nodes.
  • timemachine.scheduler.nodeName - A unique name given to a single node per a logical scheduler. A node represents a physical scheduler (JVM) instance. If you create one logical scheduler with multiple nodes, then you forms a cluster of servers that process single scheduler logic. This is an option entry, and it default to same name as the schedulerName when you only have one node defined.

Core Services Config Props

Our scheduler is made up of many core system services that you may swap implementations if you choose to do so. Here are the list:

  • timemachine.scheduler.dataStore.class - A data store implementation. Currently we provide timemachine.scheduler.service.MemoryDataStore and timemachine.scheduler.hibernate.HibernateDataStore
  • timemachine.scheduler.classLoader.class - You may provide your own class loader to load JobTask during job run. Default will use the Thread's context class loader.
  • timemachine.scheduler.jobTaskFactory.class - This will allow you to customize how the JobTask will be instantiated.
  • timemachine.scheduler.scheduleRunner.class - This control how scheduler will check and select schedules to be run.
  • timemachine.scheduler.scheduleRunner.pollingInterval - Our PollingScheduleRunner implementation allow you to set a interval in milliseconds to control how often to poll the data store for schedules to run. This does not mean your schedule will skip run between interval. It only a stop gap for our implementations to ensure that a refresh pooling interval will be invoked. If schedules are to be run in between, they will be executed according to their run time. But this interval value is same as the max allowed missed run interval, so careful not to set too high if you are not willing to see big missed run gap allowed.
  • timemachine.scheduler.scheduleRunner.maxSchedulesPerInterval - Our PollingScheduleRunner implementation also allows you to limit max schedules to retrieve from the data store per interval processing.
  • timemachine.scheduler.jobTaskPoolNameResolver.class - If you create more than two jobTask pool, you will use this class to provide job def name to pool mapping resolution.

Thread Pools Config Props

You may configure and use all of the following parameters for each pool you create.

timemachine.scheduler.jobTaskThreadPool.<uniquePoolName>.class = <threadPoolImplClass>
timemachine.scheduler.jobTaskThreadPool.<uniquePoolName>.minSize = 0
timemachine.scheduler.jobTaskThreadPool.<uniquePoolName>.maxSize = 4
timemachine.scheduler.jobTaskThreadPool.<uniquePoolName>.timeToLive = 300000
timemachine.scheduler.jobTaskThreadPool.<uniquePoolName>.useShutdownNow = false
timemachine.scheduler.jobTaskThreadPool.<uniquePoolName>.maxShutdownWaitTime = 1000
timemachine.scheduler.jobTaskThreadPool.<uniquePoolName>.threadNamePrefix = ${timemachine.scheduler.schedulerName}-JobTask-Thread-

We provide two built-in <threadPoolImplClass> already. You may use timemachine.scheduler.service.FixedSizeThreadPool or timemachine.scheduler.service.DynamicThreadPool.

  • minSize and maxSize - Used to limit the number of threads to create in a pool. For example the DynamicThreadPool will auto shrink the pool and expand depends on the jobs submitted. FixedSizeThreadPool only use maxSize value.
  • timeToLive - Is used by DynamicThreadPool to check how long an idle threads can live before it will destroy and shrink the pool.
  • useShutdownNow - When scheduler is destroyed, it will invoke the thread pool shutdown procedure. It default to false, which will make the pool wait for all threads to finish and come down gracefully. If you set this to true, it will terminate threads immediately without waiting.
  • maxShutdownWaitTime If you set useShutdownNow to false, you can still set a max wait time and ensure pool will terminated. Default is 1000 milliseconds.
  • threadNamePrefix Each thread that this pool creates will have this prefix as name plus a dynamic <threadNumber> as suffix.

You must configure at least two thread pools in the scheduler. The first one is the system thread pool, and by default, you only need one thread size set for the SchedulerRunner service.

The second thread pool you must configure is for running the job task. You can actually create more than one thread pool to run job task! When creating more than two pool, ensure the <uniquePoolName> is unique, and that you give all job definitions a name to match for which pool to pick. You may control the name to pool matching using this config format:

timemachine.scheduler.jobTaskPoolNameResolver.poolName.<uniquePoolName>.matchToJobNameRexp = <jobNameInRegExpPattern>

Any job task that didn't matched to a specific pool will default to the DEFAULT pool.

User Services Config Props

TimeMachine allow you to create any custom user timemachine.scheduler.Service, and you may create these with following format:

timemachine.scheduler.userservice.<uniqueServiceName>.class = <yourServiceClass>

Here are few examples:

timemachine.scheduler.userservice.01MyFirst.class = timemachine.scheduler.example.MyFirstUserService
timemachine.scheduler.userservice.02MyFancy.class = timemachine.scheduler.example.MyUserService
timemachine.scheduler.userservice.03MyFancy2.class = timemachine.scheduler.example.MyUserService

You would need to ensure <uniqueServiceName> is unique per key entry, and they are created as sorted order.

TIPS: You may combine all the main scheduler properties and any user services properties (eg: CrontabService or JobLoaderService etc) together into one single config properties file. All of the setting entries should have a unique key on the left.

EventHistoryService Config Props

TimeMachine allow you to record all the scheduler events, including job run history, into the DataStore service. This features is disabled by default, and you may enable it by the following configuration keys:

# Scheduler Event History Service (604800000 ms = 7 days)
timemachine.scheduler.eventHistory.class = timemachine.scheduler.service.EventHistoryService
timemachine.scheduler.eventHistory.removeInterval = 604800000

The above config settings will enable scheduler to listen to all events such as when it init, start, stop, or destroy. Also any data such as when a JobDef or Schedule has been added or deleted. All the job run events such as before and after the job is ran, an exception is thrown, or even missed run is detected events. All these are recorded per data entries into our datastore service using EventHistory data model object. This works for both MemoryDataStore as well as the HibernateDataStore.

When enable this feature, the history records can quickly accumulate and use up disk space (or RAM if you are using MemoryDataStore!) We provide a quick solution and that's to auto remove these record after a certain period. Use the removeInterval config to set this time interval in millisecond unit.

Optionally you may also add two more keys with event history service. For example:

timemachine.scheduler.eventHistory.filterTypes = SCHEDULER,DATA
timemachine.scheduler.eventHistory.filterNames = jobRunAfter

The "filterTypes" or "filterNames" is a CSV to filter out any of these values:

TYPE            NAMES
----            -----
SCHEDULER       init, start, stop, destroy
DATA            jobDefAdded, jobDefDeleted, scheduleAdded, scheduleDeleted
JOBRUN          jobRunBefore, jobRunAfter, jobMissedRun, jobRunException, schedulePaused, scheduleResumed

Note that when filters are omitted, then all events will be recorded.

Database Config Props

We have a separated HibernateDataStoreConfig page on how to configure and setup scheduler with database persistence.

Variable substitution in config properties

The config properties file you use in TimeMachine Scheduler will support variable substitution in ${key} format. It will expand into any value that you have previously defined, or one from Java System Properties. This means you can do following:

# my-config.properties
app = Cool App
msg = Hello ${app}
myscript = ${scriptName}

With above, you can supply the scriptName on command such as this:

$ bin/scheduler.sh -DscriptName=/path/to/cool-script.js my-config.properties

You also see how we use this in our default config file as well.

Logging Config Properties

The TimeMachine uses the SLF4j for logging, and it allows you to choose many different logger implementation. The default zip package comes with Log4J, and there is a config/log4j.properties file ready for you to use. For example, you may add %t in following line to see the thread names in logging output:

log4j.appender.CONSOLE.layout.conversionPattern=%d{HH:mm:ss} %t %p| %m%n

For more options, see Log4J Manual

Scripting Scheduler

The TimeMachine scheduler has built-in scripting language support. TimeMachine uses the standard Java scripting engine API, and by default, you may use JavaScript as the implementation to explore the scheduler. You may also easily add any other scripting languages such as Groovy, JRuby, or Jython etc with the scheduler. Simply add their required jars into the CLASSPATH and change the engine name, and you have the full power of scripting at your fingertips.

TimeMachine scheduler supports the scripting in two way:

1. Use the timemachine.scheduler.userservice.ScriptingService. You may configure that scheduler to run any script files upon the scheduler's init, start, stop or destroy lifecycles. Simply add this in your scheduler config properties file:

timemachine.scheduler.userservice.scripting.class = timemachine.scheduler.userservice.ScriptingService
ScriptingService.scriptEngineName = JavaScript
ScriptingService.initScript = /path/to/my-init-script.js
ScriptingService.startScript = /path/to/my-start-script.js
ScriptingService.stopScript = /path/to/my-stop-script.js
ScriptingService.destroyScript = classpath:///my-destroy-script.js

You may use just one of the entry if that's all you need. Notice that you may also use classpath prefix to where the location of the script!

Inside your script you will have many implicit variables ready for you to use to interact with the scheduler. See the class javadoc for up-to-date docs. Here is a JavaScript exmaple:

// These are the implicit variables available to the script.
logger.info("configProps=" + configProps);
logger.info("scheduler=" + scheduler");
logger.info("coreServices.dataStoreService=" + coreServices.getDataStoreService()");
logger.info("initScript=" + initScript");
logger.info("startScript=" + startScript");
logger.info("stopScript=" + stopScript");
logger.info("destroyScript=" + destroyScript");

2. Use the timemachine.scheduler.jobtask.ScriptingJobTask. You may configure this job to run with any Schedule! The job would allow you to run a snippet of scripting text, or a reference to a external script file. Here is an example on how to configure and run it using JobLoader

timemachine.scheduler.userservice.jobLoader.class = timemachine.scheduler.userservice.JobLoaderService
JobLoaderService.01myJob.timemachine.scheduler.jobtask.ScriptingJobTask = JobProps{scriptEgnineName=JavaScript|scriptText=1+1}; RepeatSchedule{interval=5|intervalUnit=MINUTE}
JobLoaderService.02myJob.timemachine.scheduler.jobtask.ScriptingJobTask = JobProps{scriptEgnineName=JavaScript|scriptFile=/path/to/my-script.js}; RepeatSchedule{interval=5|intervalUnit=MINUTE}

Again, here are the JavaScript example of implicit variables:

// These are the implicit variables available to the script.
logger.info("job=" + jobContext.getJob());
logger.info("schedule=" + jobContext.getSchedule());
logger.info("scheduler=" + jobContext.getScheduler());

TIPS: Our default binary distribution bundle with Groovy scripting engine! You may use this by simply replace above examples scriptEngineName=Groovy and with any Groovy code. See http://groovy.codehaus.org for more information.

Release tag format

We use a three release numbers format for our software tagging. You may quickly identify our changes by its format: <Major>.<Minor>.<BugFix>

  • Major - A number to signify there are new API and system architectural changes. The API might not be backward compatible from older release.
  • Minor - A number to signify there are new features added. There should be no public API change, and it should be compatible within the same major release.
  • BugFix - A number to signify there are new bug fixes applied within the same minor release.

We also use -SNAPSHOT in our version during development, and it's only for people who want to try the latest code or patches. You may try these if you can't wait for full stable release from above, or start earlier development of your own services.

Developer Guide: Writing JobTask and User Service

Basic Requirement

To develop with scheduler, you would need JDK6 or higher, and we recommend you use Maven3+ to build your project.

Setup New Maven Project

To extends and write JobTask in Java, it's best you setup a Maven project, and use the following simple pom.xml to get started. If you want to use other build tool, then skip this, and go to code sample below.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>scheduler-demo</groupId>
	<artifactId>scheduler-demo</artifactId>
	<version>1.0.0-SNAPSHOT</version>

	<build>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.3.2</version>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
				</configuration>
			</plugin>
		</plugins>
	</build>

	<dependencies>
		<dependency>
			<groupId>org.bitbucket.timemachine</groupId>
			<artifactId>timemachine-scheduler</artifactId>
			<version>1.2.2</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-simple</artifactId>
			<version>1.6.1</version>
		</dependency>
	</dependencies>
</project>

First Example

Now create this example file under your project src/main/java/scheduler/demo/examples/FirstExampleMain.java.

package scheduler.demo.examples;

import timemachine.scheduler.JobDef;
import timemachine.scheduler.Scheduler;
import timemachine.scheduler.SchedulerFactory;
import timemachine.scheduler.jobtask.LoggerJobTask;

public class FirstExampleMain {
    public static void main(String[] args) {
        Scheduler scheduler = new SchedulerFactory().createScheduler();
        try {
            scheduler.init();

            // Schedule a job that runs every 15 seconds. The Demo will end in 1 minute.
            JobDef jobDef = new JobDef();
            jobDef.setJobTaskClass(LoggerJobTask.class);
            jobDef.addSchedule(Schedules.secondly(15));
            scheduler.schedule(jobDef);

            scheduler.startAndWait(60 * 1000L);
            scheduler.stop();
        } finally {
            scheduler.destroy();
        }
    }
}

Now you may try to compile and run it like this:

$ mvn compile
$ mvn exec:java -Dexec.mainClass=scheduler.demo.examples.FirstExampleMain 

You may also pass a properties config file to the factory class to load a customized scheduler programmatically like this:

Scheduler scheduler = new SchedulerFactory("/path/to/my-scheduler.properties").createScheduler();

Writing your first JobTask

You see from above that we provide you a timemachine.scheduler.jobtask.LoggerJobTask, but of course you want to write your own JobTask so you can do what you want. Here is a classic hello world example:

package scheduler.demo.examples;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import timemachine.scheduler.JobDefContext;
import timemachine.scheduler.JobDefTask;

public class MyJobTask implements JobTask {
    private static Logger logger = LoggerFactory.getLogger(MyJobTask.class);
    
    @Override
    public void run(JobContext jobContext) {
        logger.info("Hello World.");
    }

}

The JobContext will provide access to the JobDef and Schedule that representing the "job instance".

Getting the Job to run with a Schedule

Once you have a JobTask, you may create a Job definition, and then associate one or more Schedule to run it. We provided a timemachine.scheduler.Schedules factory class that would give you some commonly used scheduling need. You may create and add the schedules like this back in the FirstExampleMain code:

// repeating jobs
JobDef jobDef = new JobDef();
jobDef.setName("CommonRepeatJob");
jobDef.setJobTaskClass(MyJobTask.class);
jobDef.addSchedule(Schedules.secondly(1));
jobDef.addSchedule(Schedules.minutely(1));
jobDef.addSchedule(Schedules.hourly(1));
jobDef.addSchedule(Schedules.daily(1));
jobDef.addSchedule(Schedules.weekly(1));
scheduler.schedule(jobDef);

// one time job
jobDef = new Job();
jobDef.setName("OnetimeJob");
jobDef.setJobTaskClass(MyJobTask.class);
jobDef.addSchedule(Schedules.once());
scheduler.schedule(jobDef);

// repeat 2 times only job with 1s in between
jobDef = new Job();
jobDef.setName("TwoCountJob");
jobDef.setJobTaskClass(MyJobTask.class);
jobDef.addSchedule(Schedules.repeatCountSecondly(2, 1));
scheduler.schedule(jobDef);

You normally would only need to write a JobTask, and configure it to run with one of our existing Schedule. Writing a custom Schedule is much harder, but it is still possible if you choose to do so. Remember that a Schedule would represent a actual "job instance", and this means it will be run by a thread in the pool service.

Writing Custom Schedule with DateListSchedule

You may write custom Schedule using our DateListSchedule together with a DateListProvider implementation.

Example:

package schedulerdemo;
import java.util*;
import timemachine.scheduler.*;
import timemachine.scheduler.schedule.*;
public class MyEndOfMonthDateListProvider implements DateListSchedule.DateListProvider {
  public List<Date> getDateList(DateListSchedule schedule) {
    List<Date> result = new ArrayList<Date>();  
    Date prevDate = schedule.getPrevRun();
    if (prevDate == null)
      result.add(Schedules.endOfMonth(Schedules.time("00:00:00")));
    else
      result.add(Schedules.endOfMonth(Schedules.addMonths(prevDate, 1)));
    return result;
  }
}

With above, you simply need to invoke dateListSchedule.setDateListProvider(MyEndOfMonthDateListProvider.class). The scheduler will automatically instantiate the class and use it to generate a new list when the old one has been exhausted. Same cycle will repeat until the provider returns an empty list, or it will perpetually regenerate a new date list to be use as the schedule.

The TimeMachine even provide a ScriptingDateListProvider that you may use script and customize Schedule without compile code!

Running SchedulerServer and using configuration properties

You may write a new Main java program on every job scheduling need, or you may reuse our SchedulerServer to simply schedule the job with a config file. You may load your own custom JobTask using our JobLoaderService. Here is an sample of my-scheduler.properties file:

timemachine.scheduler.name = MyScheduler
timemachine.scheduler.threadPool.maxSize = 5
timemachine.scheduler.dataStore.class = timemachine.scheduler.service.MemoryDataStore

timemachine.scheduler.userservice.jobLoader.class = timemachine.scheduler.userservice.JobLoaderService
JobLoaderService.01myJob.scheduler.demo.examples.MyJobTask = CronSchedule{expression=0 * * * * ?}

Above script will load your MyJobTask with a CRON schedule that runs every minute. You may start the scheduler within your Maven project like this to try it out:

$ mvn exec:java -Dexec.mainClass=timemachine.scheduler.tool.SchedulerServer -Dexec.args=my-scheduler.properties

Writing custom UserService

Besides loading jobs through the scheduler config properties file, you may also setup your own custom User services. This is how you can easily extend the scheduler functionality to do whatever you like. In fact both the JobLoaderService and CrontabService you've seen above are simply a user service we provide out of the box. But you can easily write one your self. We will show you how to do that in this section.

If you want to perform some custom code before or after the scheduler started or stopped, then you will need to write your own custom user service. You simply would write a Java class that implements UserService. For example, I can write a custom user service like this:

package scheduler.demo.examples;

import timemachine.scheduler.JobDef;
import timemachine.scheduler.Scheduler;
import timemachine.scheduler.SchedulerListener;
import timemachine.scheduler.jobtask.LoggerJobTask;
import timemachine.scheduler.support.AbstractService;

public class MyUserService extends AbstractService implements SchedulerListener {

    private Scheduler scheduler;

    @Override
    public void initService() {
        logger.info("Initializing {}", this);
    }

    @Override
    public void startService() {
        logger.info("Starting {}", this);
        JobDef jobDef = new JobDef();
        jobDef.setJobTaskClass(LoggerJobTask.class);
        jobDef.addSchedule(Schedules.secondly(1));
        scheduler.schedule(jobDef);
        logger.info("Scheduled a job that runs every second.");
    }

    @Override
    public void stopService() {
        logger.info("Stopping {}", this);
    }

    @Override
    public void destroyService() {
        logger.info("Destroying {}", this);
    }

    @Override
    public void addScheduler(InternalScheduler scheduler) {
        this.scheduler = scheduler;
    }
}

With above, you can simply configure it in your scheduler properties (eg: config/user-services.properties) file:

timemachine.scheduler.userservice.myUserService.class = scheduler.demo.examples.MyUserService

And then you may run it:

$ mvn exec:java -Dexec.mainClass=timemachine.scheduler.tool.SchedulerServer -Dexec.args=config/user-services.properties"

UserService with Listeners

When writing UserService, you are optionally able to implements few of the Listener callback interfaces to receive objects provided by the scheduler at runtime. In above example, you have seen that we used SchedulerListener. In addition this, we have few more:

  • SchedulerListener - receive the instance of the Scheduler which allow you to schedule and unschedule jobs.
  • ConfigPropsListener - receive the instance of the configuration Props passed to scheduler that contains any addition config for your user service.
  • JobListenerListener - receive many callback methods such as when a job added, removed, run or exception during run etc.
  • CoreServicesListener - receive many system services such as DataStore, ScheduleRunner, and ThreadPool etc used by the scheduler that bootstrap the application.

The JobListener has many methods to implements, so we provided a convenient base adaptor JobListenerService class that provide empty implementations. You just need to override whichever methods you are interested.

Option to write UserService in Scripting Language

As a developer, instead of using Java to implement UserService, you can always try using ScriptingService to run any external scripting file as well. See User Guide above for more information on this service.

This option would not even require you to compile the Java code, and have much faster development time. Scripting is great for exploring and writing small to medium jobs. Using Scripting engine such as Groovy makes the transition from Java to Scripting almost painlessly. But for any large enterprise complex jobs, we would still recommend you use Java because its static compile nature and tooling such as IDE help produce code that's more stable. The cost is obviously little slower pace, especially if you are in a larger team. The choices are your to pick.

Advance Scheduler with Custom SchedulerFactory

Most of our System services have a interface and implementation provided. You may choose to override even these system services if you desire so. System services affect the entire scheduler application, so care must taken to ensure it's properly added and initialized. Many system services implementation can be set in scheduler configuration properties file. You may also customize and write your own scheduler factory class to even further customize system level. Try extending the SchedulerFactory, and you may override some of the create* methods to even further control your own custom scheduler implementations.

Updated