There are many times when you would want to do some sequence of actions periodically. Such as renewing your certificates, rotating the logs bring up and bringing down instances and virtual servers based on day and time etc.

Most people run the Webserver in some kind of unix which means  that they already have and are familiar with a utility that will help them do just that, the crontab.

The Sun Java System Web Server 7.0 also ships with an event scheduler that will help you set up these actions that will be  repeated in the time frame that you specify. One of the reasons for the  scheduler getting integrated into the Webserver was that we wanted to provide an administrative interface to the scheduling facility.

Since sometimes the Webserver administrators may not be the sysadmins for the machine in which the server runs on, and also because the setting up of cron in unix required access to the machine itself, It was better to  provide a scheduler that was different from the machine cron that was  specific to the Webserver alone.

Though the current Scheduler in Webserver does have an administrative remote interface now, a small short coming is that inorder to allow any kind of complex action (Any thing that requires a condition or a dependecy) you still need access to the machine in which the Webserver runs (over and  above administrative access to the Webserver). This is because the only  way you can do such things is to write it in shell script and then schedule that shell script to run.

While it would have been a great idea to provide callbacks from the  Scheduler to the wadm, it is not there currently (Unfortunately).

Moreover, you can not schedule events across the cluster but are restricted to a particular configuration for each event.

> create-event __Usage: create-event --help|-?
or create-event [--echo] [--no-prompt] [--verbose] [--no-enabled] --config=name
--command=restart|reconfig|rotate-log|rotate-access-log|update-crl|commandline
( (--time=hh:mm [--month=1-12] [--day-of-week=sun/mon/tue/wed/thu/fri/sat]
[--day-of-month=1-31]) | --interval=60-86400(seconds) )
CLI014 config is a required option.


Because it is running on the webserverd, it also means that it is machine specific (ie) the command line specified would run once in each machine. while it is desirable in some cases, it is not so in others where you just want to execute a command cluster  wide.

let us see how much wadm will be able to help us in this matter.

##### Deciding on the API

We will try and have some similarity with the crontab, also it will be nice to make the API look like a procedure that gets executed on time.

on name "\* \* \* \* \* \*" {
if {certs-expired} {
renew-selfsigned-cert
}
rotate-logs
cleanup
}


Implementation

namespace eval Cron {
variable units
variable schedule
proc on {name time script} {
array set schedule [parse_time $time] set schedule(script)$script
set Cron::schedule($name) [array get schedule] persist return {} } proc init {} { set Cron::units {second minute hour day_of_month month day_of_week} } proc parse_time time { array set parsed {} set time [validate$time]
foreach unit $Cron::units value$time {
set parsed($unit)$value
}
return [array get parsed]
}
proc persist {} {
}
proc validate {time} {
return $time } }  > source cron.tcl > Cron::on mexico "\* \* \* \*" { puts blue } > Cron::init second minute hour day_of_month month day_of_week > Cron::on blue "\* \* \* \*" { puts true} > puts$Cron::schedule(blue)
hour \* script { puts true } day_of_week {} second \* day_of_month \* month {} minute \*


We have set aside the validation and persistance for later. They are not  strictly needed for simple operations.

##### Scheduling

Now we need to find a way to get these to be invoked periodicaly, and Tcl provides just what we want in the form of after command.

> after
wrong # args: should be "after option ?arg arg ...?"


The arguments of the after are the number of milliseconds to wait and the procedure to run after that wait. so adding the after command to our script,

variable id
proc start {} {
run [clock seconds]
catch {after cancel $Cron::id} err set Cron::id [after 1000 Cron::start] } proc run {now} { foreach id [array names Cron::schedule] { puts "$id $now" } }  Here we print each scheduled entry with 1 second periodicity. all it remains to do is to change the puts to invocation after determining if the schdedule matches to the current time. Checking it out. > source cron.tcl > Cron::init second minute hour day_of_month month day_of_week > Cron::on blue "\* \* \* \*" { puts true} > Cron::start blue 1162126366 blue 1162126367 blue 1162126368  Matching the time. Now we need to match the scheduled time for each id, and invoke it if the time matches the current. ##### A bug Unfortunately due to a bug in jacl implementation of tcl, the list entered directly in the console which contains new lines is used with the newlines stripped out, ie: > puts { a b c } abc  while in tclsh tclsh>puts { a b c } a b c  Due to this reason, when you enter scripts, you will have to terminate each line by a ‘;’ ##### Minimal Cron namespace eval Cron { variable units variable schedule variable id variable fmt set Cron::units {second minute hour day_of_month month day_of_week} set Cron::fmt {%S %M %H %d %m %w} foreach u$Cron::units f $Cron::fmt { eval "proc$u {time} { clock format \\$time -format$f }"
}

proc on {name time script} {
set Cron::schedule($name) [concat [parse_time$time] "script {$script}"] return {} } proc parse_time time { array set parsed {} foreach unit$Cron::units value $time { if [llength$value] {
set parsed($unit)$value
} else {
set parsed($unit) \* } } return [array get parsed] } proc start {} { run [clock seconds] catch {after cancel$Cron::id} err
set Cron::id [after 1000 Cron::start]
}

proc run {now} {
foreach id [array names Cron::schedule] {
runone $id$now
}
}

proc runone {id now} {
array set time $Cron::schedule($id)
foreach unit $Cron::units { if {![includes$time($unit) [$unit $now]]} { return } } if [catch {eval$time(script)} err] {
puts "Error($id):$err"
}
}

proc includes {lst var} {
regsub {\^0+(.+)$}$var {\\1} var
foreach p $lst { if {[lsearch -glob$var $p] > -1} { return 1 } } return 0 } }  #### Some shortcuts. ##### You may have noticed this line foreach u$Cron::units f $Cron::fmt { eval "proc$u {time} { clock format \\$time -format$f }"
}


where I am making use of the tcl’s dynamic evaluation capabilities to create similar procedures in a loop. It allows us to abstract further and reduce duplication of code.

##### Using it
> source cron.tcl
> Cron::on blue \* {
:puts one;
:puts two;
:puts three;
:}
> Cron::start
one
two
three
one
two
three


As noted above, please insert the ‘;’ to terminate each lines.,

##### Persistance

Because we are dealing with tcl, the data always has a string representaion that can be used to recreate the data. so all it takes us to implement persistance  is

proc persist {} {
set f [open $Cron::ifile w] puts$f [array get Cron::schedule]
close $f } proc init {} { catch { set f [open$Cron::ifile r]
array set Cron::schedule [read -nonewline $f] close$f
} err
start
return {}
}


We just write the Cron::schedule to a file ‘.cron.wadm’ and read it back when we startup.

The completed cron with validation and listing is available here I have removed the seconds part from the cron since it is not very useful  except during debugging.

##### Using It
> source cron.tcl
> Cron::init
> Cron::on blue \* {
:puts 1
:}
1
1
1
1
> Cron::stop
> Cron::ls
blue
> Cron::ls -l
blue => hour \* day_of_week \* second \* day_of_month \* month \* minute \* script {puts 1}
> Cron::on newblue {{0 1} 1} {
:puts mex;
:puts mee;
:}
> Cron::rm blue
> Cron::ls -l
newblue => hour \* day_of_week \* second {0 1} day_of_month \* month \* minute \* script {puts mex;puts mee;}
....
mex
mee
mex
mee


I have removed the seconds part from the cron since it is not very useful  except during debugging.

> source cron.tcl
> Cron::init
> Cron::on blue \* {
:puts here;
:}
> Cron::ls -l
blue => hour \* day_of_week \* day_of_month \* month \* minute \* script {puts here;}
here
here


The completed cron is available here