How to schedule dynamic function with cron job?

It is possible to set up a cron job that runs every minute and when triggered it checks what jobs are scheduled for that moment.

As a simple idea which could be easily modified to push through the run time details for a particular script if you wanted:-

<?php

include '/core/config.php');

// Test script to allow jobs to be set up (cron style) on a database, but with the addition that jobs can be made
// dependent on other jobs completing first.
// Currently does not support jobs being dependent on more than one parent job.
// It uses a database of 2 tables. One for servers and the other for jobs.
// The server is selected as the one that matches the value of php_uname('n') (hence this can be run on many servers accessing a single database and only executing jobs for the particular server an instance is running on)
// Time ranges are specified in the same way as on CRON jobs:-
//  *   = no restriction based on that field
//  x   = when the value of that time parameter matches x
//  /x  = every x of that field (ie, mod current of that field by x and match if result is 0)
//  x-y = when the value of that time parameter is between x and y
//  x,y = when the value of the time parameter matches x or y (or z, etc)
// The script field on the scheduling table contains the script / command to be executed. For example if a php script then it might be 'php /usr/webdata/cron_scripts/some_script.php
// Parentid is the id of a job that must have finished before the job is executed.

class scheduling extends core_class
{

    public $connections;
    private $db;

    private $year;
    private $month;
    private $day;
    private $hour;
    private $minute;
    private $second;
    private $day_of_week;
    private $background_kick_off = true;

    private $completed_jobs = array();

    function __construct($connections, $background_kick_off = true) 
    {
        parent::__construct($connections);

        $this->background_kick_off = $background_kick_off;

        $this->debug_time_start();

        $this->connections = $connections;
        $this->db = new database($connections['EO'], 'em_scheduling');
        if (!$this->db->no_error)
            $this->error('E_ERROR', $this->db->error());

        $run_date = date('Y/m/d H:i:s w');

        list($date_part, $time_part, $this->day_of_week) = explode(' ', $run_date);
        list($this->year, $this->month, $this->day) = explode('/', $date_part);
        list($this->hour, $this->minute, $this->second) = explode(':', $time_part);     

        $this->find_jobs(0);
    }

    function find_jobs($parent_id)
    {
        $sql = "SELECT a.id, a.script, a.parent_id, a.minutes, a.hours, a.day_of_month, a.months, a.day_of_week, a.script_description, COUNT(DISTINCT b.id) AS child_count
                FROM scheduling a
                ON s.id = a.server_id
                LEFT OUTER JOIN scheduling b
                ON a.id = b.parent_id
                AND b.enabled = 1
                AND (b.minutes = '*' OR FIND_IN_SET('".$this->minute."', b.minutes) OR (SUBSTR(b.minutes, 1, 1) = '/' AND (".$this->minute." % CAST(SUBSTR(b.minutes, 2) AS UNSIGNED)) = 0) OR (b.minutes LIKE '%-%' AND ".$this->minute." BETWEEN CAST(SUBSTRING_INDEX(b.minutes, '-', 1) AS UNSIGNED) AND CAST(SUBSTRING_INDEX(b.minutes, '-', -1) AS UNSIGNED)))
                AND (b.hours = '*' OR FIND_IN_SET('".$this->hour."', b.hours) OR (SUBSTR(b.hours, 1, 1) = '/' AND (".$this->hour." % CAST(SUBSTR(b.hours, 2) AS UNSIGNED)) = 0) OR (b.hours LIKE '%-%' AND ".$this->hour." BETWEEN CAST(SUBSTRING_INDEX(b.hours, '-', 1) AS UNSIGNED) AND CAST(SUBSTRING_INDEX(b.hours, '-', -1) AS UNSIGNED)))
                AND (b.months = '*' OR FIND_IN_SET('".$this->month."', b.months) OR (SUBSTR(b.months, 1, 1) = '/' AND (".$this->month." % CAST(SUBSTR(b.months, 2) AS UNSIGNED)) = 0) OR (b.months LIKE '%-%' AND ".$this->month." BETWEEN CAST(SUBSTRING_INDEX(b.months, '-', 1) AS UNSIGNED) AND CAST(SUBSTRING_INDEX(b.months, '-', -1) AS UNSIGNED)))
                AND ((b.day_of_month = '*' OR FIND_IN_SET('".$this->day."', b.day_of_month) OR (SUBSTR(b.day_of_month, 1, 1) = '/' AND (".$this->day." % CAST(SUBSTR(b.day_of_month, 2) AS UNSIGNED)) = 0) OR (b.day_of_month LIKE '%-%' AND ".$this->day." BETWEEN CAST(SUBSTRING_INDEX(b.day_of_month, '-', 1) AS UNSIGNED) AND CAST(SUBSTRING_INDEX(b.day_of_month, '-', -1) AS UNSIGNED)))
                OR (b.day_of_week = '*' OR FIND_IN_SET('".$this->day_of_week."', b.day_of_week) OR (SUBSTR(b.day_of_week, 1, 1) = '/' AND (".$this->day_of_week." % CAST(SUBSTR(b.day_of_week, 2) AS UNSIGNED)) = 0) OR (b.day_of_week LIKE '%-%' AND ".$this->day_of_week." BETWEEN CAST(SUBSTRING_INDEX(b.day_of_week, '-', 1) AS UNSIGNED) AND CAST(SUBSTRING_INDEX(b.day_of_week, '-', -1) AS UNSIGNED))))
                WHERE a.parent_id = ".(int)$parent_id."
                AND a.enabled = 1
                AND (a.minutes = '*' OR FIND_IN_SET('".$this->minute."', a.minutes) OR (SUBSTR(a.minutes, 1, 1) = '/' AND (".$this->minute." % CAST(SUBSTR(a.minutes, 2) AS UNSIGNED)) = 0) OR (a.minutes LIKE '%-%' AND ".$this->minute." BETWEEN CAST(SUBSTRING_INDEX(a.minutes, '-', 1) AS UNSIGNED) AND CAST(SUBSTRING_INDEX(a.minutes, '-', -1) AS UNSIGNED)))
                AND (a.hours = '*' OR FIND_IN_SET('".$this->hour."', a.hours) OR (SUBSTR(a.hours, 1, 1) = '/' AND (".$this->hour." % CAST(SUBSTR(a.hours, 2) AS UNSIGNED)) = 0) OR (a.hours LIKE '%-%' AND ".$this->hour." BETWEEN CAST(SUBSTRING_INDEX(a.hours, '-', 1) AS UNSIGNED) AND CAST(SUBSTRING_INDEX(a.hours, '-', -1) AS UNSIGNED)))
                AND (a.months = '*' OR FIND_IN_SET('".$this->month."', a.months) OR (SUBSTR(a.months, 1, 1) = '/' AND (".$this->month." % CAST(SUBSTR(a.months, 2) AS UNSIGNED)) = 0) OR (a.months LIKE '%-%' AND ".$this->month." BETWEEN CAST(SUBSTRING_INDEX(a.months, '-', 1) AS UNSIGNED) AND CAST(SUBSTRING_INDEX(a.months, '-', -1) AS UNSIGNED)))
                AND ((a.day_of_month = '*' OR FIND_IN_SET('".$this->day."', a.day_of_month) OR (SUBSTR(a.day_of_month, 1, 1) = '/' AND (".$this->day." % CAST(SUBSTR(a.day_of_month, 2) AS UNSIGNED)) = 0) OR (a.day_of_month LIKE '%-%' AND ".$this->day." BETWEEN CAST(SUBSTRING_INDEX(a.day_of_month, '-', 1) AS UNSIGNED) AND CAST(SUBSTRING_INDEX(a.day_of_month, '-', -1) AS UNSIGNED)))
                OR (a.day_of_week = '*' OR FIND_IN_SET('".$this->day_of_week."', a.day_of_week) OR (SUBSTR(a.day_of_week, 1, 1) = '/' AND (".$this->day_of_week." % CAST(SUBSTR(a.day_of_week, 2) AS UNSIGNED)) = 0) OR (a.day_of_week LIKE '%-%' AND ".$this->day_of_week." BETWEEN CAST(SUBSTRING_INDEX(a.day_of_week, '-', 1) AS UNSIGNED) AND CAST(SUBSTRING_INDEX(a.day_of_week, '-', -1) AS UNSIGNED))))
                GROUP BY a.id, a.script, a.parent_id, a.minutes, a.hours, a.day_of_month, a.months, a.day_of_week
                ORDER BY child_count
                ";

        //echo "\r\n $sql \r\n";

        $this->db->query($sql) or die($this->db->error());

        $process_array = array();

        while ($row = $this->db->fetch_assoc())
        {
            $process_array[] = $row;
        }   

        foreach($process_array as $aProcess)
        {
            if ($this->background_kick_off and $aProcess['child_count'] == 0)
            {
                // No jobs to follow so just kick them off as a background task
                $this->launchBackgroundProcess($aProcess['script']);
                $completed_jobs[$aProcess['id']] = $aProcess['script_description'];
            }
            else
            {
                passthru($aProcess['script'].'', $return_var);
                if ($return_var == 0)
                {
                    $completed_jobs[$aProcess['id']] = $aProcess['script_description'];
                    $this->find_jobs($aProcess['id']);
                }
            }
        }
    }

    private function launchBackgroundProcess($call) 
    {

        // Windows
        if($this->is_windows())
        {
            pclose(popen('start /b '.$call, 'r'));
        }

        // Some sort of UNIX
        else 
        {
            pclose(popen($call.' /dev/null &', 'r'));
        }
        return true;
    }

    private function is_windows()
    {
        if(PHP_OS == 'WINNT' || PHP_OS == 'WIN32')
        {
            return true;
        }
        return false;
    }
}

$Scheduling = new scheduling($connections, true);

?>

Tables like this:-

CREATE TABLE IF NOT EXISTS `scheduling` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `enabled` tinyint(1) NOT NULL DEFAULT '1',
  `script` varchar(255) DEFAULT NULL,
  `parent_id` int(11) DEFAULT NULL,
  `minutes` varchar(5) DEFAULT NULL,
  `hours` varchar(5) DEFAULT NULL,
  `day_of_month` varchar(5) DEFAULT NULL,
  `months` varchar(5) DEFAULT NULL,
  `day_of_week` varchar(5) DEFAULT NULL,
  `script_description` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `parent_id` (`server_id`,`parent_id`,`enabled`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 AUTO_INCREMENT=15 ;

--
-- Dumping data for table `scheduling`
--

INSERT INTO `scheduling` (`id`,  `enabled`, `script`, `parent_id`, `minutes`, `hours`, `day_of_month`, `months`, `day_of_week`, `script_description`) VALUES
(1, 1, 'php download.php', 0, '*', '*', '*', '*', '*', 'Download files'),
(2, 1, 'php load_data.php', 1, '*', '*', '*', '*', '*', 'Load files to database'),
(3, 1, 'php file_test.php', 1, '*', '*', '*', '*', '*', NULL),
(4, 1, 'php file_test.php', 1, '*', '*', '*', '*', '*', NULL),
(5, 1, 'php file_test.php', 1, '*', '*', '*', '*', '*', NULL),
(6, 1, 'php file_test.php', 1, '*', '*', '*', '*', '*', NULL),
(7, 1, 'php file_test.php', 1, '*', '*', '*', '*', '*', NULL),
(8, 1, 'php file_test.php', 1, '*', '*', '*', '*', '*', NULL),
(9, 1, 'php file_test.php', 1, '*', '*', '*', '*', '*', NULL),
(10, 1, 'php file_test.php', 1, '*', '*', '*', '*', '*', NULL),
(11, 1, 'php file_test.php', 1, '*', '*', '*', '*', '*', NULL),
(12, 1, 'php file_test.php', 1, '*', '*', '*', '*', '*', NULL),
(13, 1, 'php file_test.php', 1, '*', '*', '*', '*', '*', NULL),
(14, 1, 'php file_test.php', 1, '*', '*', '*', '*', '*', NULL);

Cron tasks require you to preset the times at which they run, they cannot (yes you could hack this by having a script which edits your crontab, but I would not say that is a very good idea) have their time to run decided dynamically. This means you essentially have two options:

1) Set a cronjob to run every minute and use a temp file which you touch to tell the last time that it ran one of the scheduled tasks Each time that it runs it checks if there was a task to run in between the last timestamp of your temp file and the current time, and if there is it runs the task. This is a gross but simple solution.

2) Don't use cron. Create a daemon which checks what times tasks need to be run and puts them into a priority queue, then it pops the earliest element and sleeps until it is time to run that task. It runs the task and reinserts it to be run 24 hours in the future and repeats. This solution is by far more elegant, but it also requires more work.


you have 2 ways, although only one will do exactly what you want to do;

  • 1st way requires that you have access and privileges to change cron-jobs server side (example via PHP or other). Depending on what OS there are tutorials: Win , Nix

  • 2nd way will do something close to what you want but without the minutes precision, you will loose at max 2 minutes each cycle. (see exaplanation below).

1st Way perfect way

  • As soon as the user hit the form create a unique cron-task for that user using the desired datatime.

if you don't have those privileges you can use 3d part service like www.easycron.com they also offer a Free version with limited query. they also provide a REST API method to manage (CRUDE) cron-tasks.

2nd Way imperfect way

  • add a new VARCHAR column, i called it today with this we will ensure that the task will run only once per day.

-

+----+---------+----------+------------+--------+----------+
| iD | timeDay | timeHour | timeMinute | postiD |   today  |
+--------------------------------------+--------+----------+
| 1  | *       | 9        | 0          | 21     | 30-05-04 |
|----|---------|----------|------------|--------|----------+
| 2  | *       | 10       | 30         | 22     |          |
|----|---------|----------|------------|--------|----------+
| 3  | *       | 11       | 0          | 23     |          |
+----|---------+----------+------------+--------+----------+
  • after that create a php file i called it crontask.php we will call it each 5 minutes

  • add this to your cronjob panel:

  • 0,5 * * * * /usr/bin/php /www/virtual/username/crontask.php > /dev/null 2>&1

  • in the crontask.php file

-

<?php
// include() Database Config file here with mysql_connect etc...
// include() the required files ex. the file where myFunction reside...

$cron_cycle = 5; // set it equal to what used in cron command-line
$today = date('Y-m-d');
if($result = mysql_query("SELECT * FROM postDataTime WHERE today != '{$today}'")){
    while ($row = mysql_fetch_array($result, MYSQL_ASSOC)) { 
        $postID = $row['postID'];
        $timeHour = (int) $row['timeHour'];
        $current_hours = (int) date('H'); // current hours
        $current_minutes = (int) date('i'); // current minutes
        $timeMinute = (int) $row['timeMinute'];
        // force to run at the closest cycle
        $timeMinute = ($timeMinute % $cycle === 0) ? $timeMinute : toCloser($timeMinute, $cron_cycle); 
        if( $current_hours === $timeHour && $current_minutes === $timeMinute ){
            // ensure that we have already runned a cron for this user...
            mysql_query("UPDATE postDataTime SET today = '{$today}' WHERE postID = '{$postID}'");
            myFunction($postID);
        }
    }
}
function toCloser($n,$x=5) {
    $j = (round($n)%$x === 0) ? round($n) : (round(($n+$x/2)/$x)*$x);
    return ($j-$n) >= round($x/2) ? ($j-$x) : $j;
}

?>

Explanation of the function:

Assuming that the cron shedule runs each 5 minutes, lat's say we are at 20:00 o'clock, now the cron will run at 20:05, 20:10, 20:15, 20:20 and so on...

then assuming in our DB we have those time

Jonh  : 20:05, 
Mario : 20:32, 
luke  : 20:48, 
David : 20:57, 
Jimmy : 20:06, 
Eddy  : 20:16

when the script checks against those times it will run as below:

at 20:05 -> run 20:05 Jonh, 20:06 Jimmy
at 20:10 -> run null
at 20:15 -> run 20:16 Eddy
at 20:20 -> run null
and so on....

As you see you would loose in the worst case 2 minutes each time. I think it's fair enough! ;)