This started as a stripped down version of clockwork.
Tickwork provides a familiar and compatible config file for scheduled jobs, but instead of it being driven by a background process, it relies on regular calls to Tickwork.run
. Tickwork.run
efectively ticks the clock forward from the last time it was called scheduling jobs as it goes. By tuning the paramters below, you can call Tickwork.run
as little or as often as you like.
Tickwork keeps track of time using a datastore. This is compatible with ActiveSupport::Cache::Store
so should be compatible with most cache stores out there. It is important the cache be distributed (so not ActiveSupport::Cache::Store
). AWS_Tickwork contains an example of an ActiveRecord
implementation.
Note that clockwork allowed schedules to be dynamically set via the database. This functionality does not exist in Tickwork.
This was originally built with the idea of using Cloudwatch Events to power time. An implementation doing that is here: AWS_Tickwork. Another option would be Heroku Scheduler. I'm sure there are many more options out there.
Create tick.rb:
require 'tickwork'
module Tickwork
configure do |config|
# See DataStore below
config[:data_store] = MyDataStore
end
handler do |job|
puts "Running #{job}"
end
# handler receives the time when job is prepared to run in the 2nd argument
# handler do |job, time|
# puts "Running #{job}, at #{time}"
# end
every(10.seconds, 'frequent.job')
every(3.minutes, 'less.frequent.job')
every(1.hour, 'hourly.job')
every(1.day, 'midnight.job', :at => '00:00')
end
Note, this needs to be global to access the config whenever you run Tickwork. If you're on rails, this should be an initializer.
If you need to load your entire environment for your jobs, simply add:
require './config/boot'
require './config/environment'
under the require 'tickwork'
declaration.
Then, somewhere else in your app, you need to regularly call Tickwork.run
.
The clock process only makes sense as a place to schedule work to be done, not to do the work. It avoids locking by running as a single process, but this makes it impossible to parallelize. For doing the work, you should be using a job queueing system, such as Delayed Job, Beanstalk/Stalker, RabbitMQ/Minion, Resque, or Sidekiq. This design allows a simple clock process with no locks, but also offers near infinite horizontal scalability.
For example, if you're using Beanstalk/Stalker:
require 'stalker'
module Tickwork
handler { |job| Stalker.enqueue(job) }
every(1.hour, 'feeds.refresh')
every(1.day, 'reminders.send', :at => '01:30')
end
Using a queueing system which doesn't require that your full application be loaded is preferable, because the clock process can keep a tiny memory footprint. If you're using DJ or Resque, however, you can go ahead and load your full application enviroment, and use per-event blocks to call DJ or Resque enqueue methods. For example, with DJ/Rails:
require 'config/boot'
require 'config/environment'
every(1.hour, 'feeds.refresh') { Feed.send_later(:refresh) }
every(1.day, 'reminders.send', :at => '01:30') { Reminder.send_later(:send_reminders) }
:at
parameter specifies when to trigger the event:
HH:MM
H:MM
**:MM
HH:**
(Mon|mon|Monday|monday) HH:MM
The simplest example:
every(1.day, 'reminders.send', :at => '01:30')
You can omit the leading 0 of the hour:
every(1.day, 'reminders.send', :at => '1:30')
Wildcards for hour and minute are supported:
every(1.hour, 'reminders.send', :at => '**:30')
every(10.seconds, 'frequent.job', :at => '9:**')
You can set more than one timing:
every(1.day, 'reminders.send', :at => ['12:00', '18:00'])
# send reminders at noon and evening
You can specify the day of week to run:
every(1.week, 'myjob', :at => 'Monday 16:20')
If another task is already running at the specified time, clockwork will skip execution of the task with the :at
option.
If this is a problem, please use the :thread
option to prevent the long running task from blocking clockwork's scheduler.
:tz
parameter lets you specify a timezone (default is the local timezone):
every(1.day, 'reminders.send', :at => '00:00', :tz => 'UTC')
# Runs the job each day at midnight, UTC.
# The value for :tz can be anything supported by [TZInfo](http://tzinfo.rubyforge.org/)
:if
parameter is invoked every time the task is ready to run, and run if the
return value is true.
Run on every first day of month.
Tickwork.every(1.day, 'myjob', :if => lambda { |t| t.day == 1 })
The argument is an instance of ActiveSupport::TimeWithZone
if the :tz
option is set. Otherwise, it's an instance of Time
.
This argument cannot be omitted. Please use _ as placeholder if not needed.
Tickwork.every(1.second, 'myjob', :if => lambda { |_| true })
By default, clockwork runs in a single-process and single-thread. If an event handler takes a long time, the main routine of clockwork is blocked until it ends. Tickwork does not misbehave, but the next event is blocked, and runs when the process is returned to the clockwork routine.
The :thread
option is to avoid blocking. An event with thread: true
runs in a different thread.
Tickwork.every(1.day, 'run.me.in.new.thread', :thread => true)
If a job is long-running or IO-intensive, this option helps keep the clock precise.
Tickwork exposes a couple of configuration options:
By default Tickwork logs to STDOUT
. In case you prefer your
own logger implementation you have to specify the logger
configuration option. See example below.
This is the default timezone to use for all events. When not specified this defaults to the local timezone. Specifying :tz in the parameters for an event overrides anything set here.
Tickwork runs handlers in threads. If it exceeds max_threads
, it will warn you (log an error) about missing
jobs.
Boolean true or false. Default is false. If set to true, every event will be run in its own thread. Can be overridden on a per event basis (see the :thread
option in the Event Parameters section above)
This prefixes keys with a namespace which is useful to prevent colisions if you are using redis or memcache as the datastore. Defautls to _tickwork_
.
Think about Tickwork as having a concept of now built into it, but rather than now moving with the clock, it only moves forward (ticks forward) when you tell it to. You tell it to tick forward through time by calling Tickwork.run
. How much it ticks forward is controlled by the following variables.
If you think of a clock, each tick is 1 second, and you take 1 tick each second. With tickwork, you control the size of the ticks, how many you take, and how often you take them.
Tickwork will never tick into the future.
This is the interval in seconds that each tick will step forward. The original clockwork implementation would (by default) wake up every second to check for work. Tickwork defaults to 60 seconds. This effectively puts a floor on your frequency of events you can schedule. So if you scheduled something to run every 30 seconds, it would only be run every other time -- so don't do that.
In general, set this to at least as small as your most frequently run job. If you set this to a value larger than 60, then events schedule to run at a particular time may be missed.
This is the most number of ticks executed per run. If you have tick_size
set to 60, then each tick will be 1 minute. If max_ticks
is set to 10, then a call to Tickwork.run
could result in as many as 10 minutes worth of jobs being scheduled. If you had 1 job that ran every minute, up to 10 jobs would be run. Tickwork will not tick into the future, so you may run fewer than this number of jobs.
In any given call to Tickwork.run
, you can move foward through time at most tick_size * max_ticks
When running tickwork, the last time you run it is important since that is what now. But what if your system goes down? max_catchup
sets a floor on how far back Tickwork look back for jobs. This defaults to 3600 which is 1 hour. This means that if you run Tickwork for a day, then turn your system off for a day, then start running Tickwork again, it will start scheduling jobs from 1 hour ago.
Setting to 0 or nil disables the feature, and Tickwork will start from where it left off.
If there is no last timestamp, Tickwork starts from now.
This must be larger than your tick_size
, and probably significantly larger to avoid missing any jobs.
A datastore is required to save the times that jobs last run. This is how Tickwork keeps track of time. This can be anything that implements the following methods:
def read(key)
end
def write(key, value)
end
ActiveSupport::Cache::Store satisfies this, so Rails users can use that. This must be a shared cache to work properly in an environment with multiple servers.
module Tickwork
configure do |config|
config[:logger] = Logger.new(log_file_path)
config[:tz] = 'EST'
config[:max_threads] = 15
config[:thread] = true
config[:tick_size] = 60
config[:max_ticks] = 10
config[:max_catchup] = 3600
end
end
Since tickwork requires on some external system to make calls into Tickwork.run
, you must balance whatever that system is against the config settings.
Lets say you call Tickwork.run
every 5 minutes and you have no jobs trying to run faster than 1x/min. The default values will work well (tick_size: 60, max_ticks: 10
). Every 5 minutes, you would expect to run 5 minutes worth of jobs. If you miss 1 period, you will catch up and run 10 minutes worth of jobs. However, if you miss 2 periods, then call back (after 15 min), it will take 2 calls to catch up since there are 15 minutes waiting to run, but max_ticks
limits this to just 10 per call.
You can add error_handler to define your own logging or error rescue.
module Tickwork
error_handler do |error|
Airbrake.notify_or_ignore(error)
end
end
Current specifications are as follows.
- defining error_handler does not disable original logging
- errors from error_handler itself are not rescued, and stop clockwork
Any suggestion about these specifications is welcome.
tick.rb is standard Ruby. Since we include the Tickwork module, this exposes a small DSL to define the handler for events, and then the events themselves.
The handler typically looks like this:
handler { |job| enqueue_your_job(job) }
This block will be invoked every time an event is triggered, with the job name passed in. In most cases, you should be able to pass the job name directly through to your queueing system.
The second part of the file, which lists the events, roughly resembles a crontab:
every(5.minutes, 'thing.do')
every(1.hour, 'otherthing.do')
In the first line of this example, an event will be triggered once every five minutes, passing the job name 'thing.do' into the handler. The handler shown above would thus call enqueue_your_job('thing.do').
You can also pass a custom block to the handler, for job queueing systems that rely on classes rather than job names (i.e. DJ and Resque). In this case, you need not define a general event handler, and instead provide one with each event:
every(5.minutes, 'thing.do') { Thing.send_later(:do) }
If you provide a custom handler for the block, the job name is used only for logging.
You can also use blocks to do more complex checks:
every(1.day, 'check.leap.year') do
Stalker.enqueue('leap.year.party') if Date.leap?(Time.now.year)
end
In addition, Tickwork also supports :before_tick
and after_tick
callbacks.
They are optional, and run every tick (a tick being whatever your :sleep_timeout
is set to, default is 1 second):
on(:before_tick) do
puts "tick"
end
on(:after_tick) do
puts "tock"
end
Feel free to add your idea or experience and send a pull-request.
Created by Adam Wiggins
Inspired by rufus-scheduler and resque-scheduler
Design assistance from Peter van Hardenberg and Matthew Soldo
Patches contributed by Mark McGranaghan and Lukáš Konarovský
Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
http://github.com/tomykaira/clockwork http://github.com/softwaregravy/tickwork