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