Controlling Time - Programming without Cron Alternatives - Elixir PHP Tcl Virtualmin

Controlling Time - Programming without Cron Alternatives - Elixir PHP Tcl Virtualmin

Show table of contents

My adventures with Cron began almost 15 years ago. It all started with PHP text based games I was creating and managing.

Since then I've went through many phases where I had implemented similar functionality in the software itself.
I will show you what other options I have devised in Elixir, PHP and TCL during this article.

My latest encounter was connected to well, you guessed it, PHP again. I was setting up Sendy newsletter automation. It required 2 cronjobs to function properly. One for the newsletter sending each minute so it doesn't send all mails at once. The second one was for autoresponders to be sent out to new users upon registration and at predefined times. This is a cool feature since you're sure not to attract negative spam AND can effectively send thousands of mails per day.

I was using Virtualmin on my own VPS. I didn't want to setup the cron jobs under root for security reasons. I set them up under the user where it was hosted.
But it kept having an issue and the cron jobs didn't run, if you want to find out the issue and solution read the Virtualmin Cron Issue and Solution section.

Cron's importance

Cron is a valuable tool for running scheduled tasks. It can be used to program certain tasks on predefined times.
It will run the script/program you have written to do the task itself. You can use compiled software or run interpreted programs aswell.

You can even run bash scripts. Cron doesn't care. It will run everything at predefined moments for you.

Cron is gold since it takes care of a very problematic issue with software which can't take care of it's own internal scheduling. It's also gold since you can program Cron to send you e-mails in case something goes wrong. A sort of failsafe.

More on the cron syntax can be learned by typing man cron in your commandline on Linux.

What tasks are people using cron for?

Most of the time I've seen cron being used for tasks like:

  1. Cleaning up a temporary folders, uploads. Being sure there will be enough disk space afterwards
  2. Changing the status of rows in a SQL database if a time has exceeded. For 90% of the cases you would'nt need cron but sometimes you may want to archive certain rows automatically or move them from a table to another table automatically and reindex things. This will ensure a proper functioning of the database
  3. Sending of a summary e-mail once a day. This is useful for getting admin mails on things that happened on your website/blog/platform.
  4. Critical Alerts in case of a database connection not working, a certain service is not working anymore or won't start up. Sending yourself mails or even sending SMS'es if the website is down might be a great idea. Yet I wouldn't recommend using cron for this. You might want to look over mmonit which can do this way better.

Fallacies of Cron and what might go wrong

Cron jobs can fail if the script gives an error and it's uncaught.
Cron Jobs can also fail in the fateful event of a backup being put back on another server while forgetting to set up the cron job again.

Imagine the old developer had 3 cron jobs setup. IN the meanwhile they quit so you're hired to keep up with the changes.
You had no idea there was a cron job and the company is in the midst of migrating everything to new hardware which means reinstalling everything. They're also in the midst of setting up new provisioning software.

Everything works fine for 2 months until you get a call late Friday night. Some production servers are throwing errors and customers can't order anything.
Your weekend hiking expedition is ruined. You need to spend time on your laptop while others enjoy the beautiful landscape around you.

As you investigate you see that they've run out of hard disk space and RAM. Inexplicably! No one knows what has happened. People think it's you and the software development team who did something wrong. Your team blames it on the Q&A testing teams and no has an idea what they can or cannot do, delete or move.
The Security Officer thinks there is a breach and you might have malware on your servers.
Should you call the police? Who's to blame?

The truth is the old developer had setup a cron job to cleanup the disk space every day. This means a few gb might get cleaned up every day.
After 2 days of weekend crisis you decided to call the old developer to ask a few questions maybe he will have an idea. Once you explain that the disks are full he simply asks you: 

"Did you setup the cron jobs?"

That moment you realize there was some code to do something it was never being run. So another team member had deleted it in the practice of code refactoring.
You go through all the commits and find it and spend your time implementing the code back together with a cronjob just to make it work until you come back to the office to implement a proper NON CRON ALTERNATIVE.

Cron jobs can fail if anyone forgets to use them. People are moving from hosting to hosting. MOst of the users of PHP and Wordpress may forget they need to setup cron jobs.

There are alternatives, some alternatives which I've discovered.

Virtualmin Cron Issue and Solution

If you're using Virtualmin or Webmin to setup cron jobs please be aware of the following issue which I've encountered.
Cron jobs are not executed properly under other users other than root. They don't run.

Guess what, even though when I clicked "run now" it worked, no errors in cron syntax nothing. it seemed that it wasn't run at the predetermined time.
Virtualmin is creating a crontabfile in another place which doesn't get run by the user itself.

The solution

Either run your cron jobs as Root (not advised). Or go into the commandline to edit the crontab file of the user manually adding your cron jobs.

PHP Cron programming Alternatives

PHP alternatives to cron can be done in pure PHP and have the following benefits:

  • NO external software to be installed.
  • No other libraries
  • Pure PHP
  • You can use any database which can interract with PDO, this example runs with SQLIite for the sake of simplicity

You can use the following script anywhere, anytime, anyhow!

All you need for it to work is be sure that someone visits your website at least once a day or every x predefined time.

<?php

$dbFilename = "6eafc6f7f41b5957ab228dd51c6d437e.sqlite";

    //open the database
$db = new PDO("sqlite:{$dbFilename}");
//TODO verify if db is not null..etc
function createDataBase() {
	global $db, $dbFilename;
	if (filesize($dbFilename) <= 1) {
		echo 'Creating database...<br>';
		$db->exec("CREATE TABLE task_scheduler (id INTEGER PRIMARY KEY,  task_name TEXT,  task_description TEXT, run_every INT, last_run INT)");   
		$db->exec("INSERT INTO task_scheduler VALUES (1,'php_task','Recurrent task',60,10000)");
	}
}
createDataBase();

function updateTask($name,$last_run) {
	global $db;
	$sth = $db->prepare('UPDATE task_scheduler SET last_run = :last_run WHERE task_name = :name');
	$sth->bindParam(':last_run', $last_run);
	$sth->bindParam(':name', $name);
	$sth->execute();
	return $sth->rowCount();
}
function getTask ($name) {
	global $db;
	$stmt = $db->prepare('SELECT * FROM task_scheduler WHERE task_name = :name');
	$stmt->bindParam(':name', $name);
	$stmt->execute();
	return $stmt->fetch();
}

function runTask() {
	$task = getTask('php_task');
	$date  = new DateTime();

	if (($task["last_run"] + $task["run_every"]) <  $now = $date->getTimestamp()) {
		$next = $now + $task["run_every"];
		echo "Running very intensive task <br>" ;
		echo "Complete! Next one at " .  date("d-m-Y H:i:s", $next) . " <br>";
		echo updateTask('php_task',$now).  " records UPDATED successfully";
	} else {
		echo "Not running any scheduled task. Next task at " . (($task["last_run"] + $task["run_every"]))  . ", processing can go on<br>";
	}

}

echo " <h1>Task Scheduler</h1><br>";
runTask();
echo "Have a nice day!<br>";
 $db = NULL;
 

The first time you'll run the example it's going to create the database and insert the recurrent task. Then it's going to read it and execute it. Updating the task run time.
If you refresh the page it will not run the scheduled task anymore until the time has passed.

You can create much more complex solutions in pure PHP. This doesn't require any specific library to overcomplicate things.
Do you have 1000 people visiting per minute or are concerned about certain speed impediments?
You can change the function to read from a text file or from a key-value memory database like redis or memcached.
Be creative, do something else! Also be careful, don't do too many DB intensive reads/writes at each visit.

NOTE: This alternative doesn't work when you want to send yourself a status e-mail at night if you don't have any visitors then.

Elixir cron schedule alternatives

I've found that the Elixir approach to scheduling recurrent jobs is actually ideal. Due to the fact that Elixir can actually help us in scheduling by leveraging the power of processes. Processes which can control their own state.

With the Process.send_after command we can schedule a function call to occur at a predefined time. Take a look at the following GenServer example where you can specify when it should first start the cron and at which interval it should run a command.

defmodule MyCoolApp.Scheduler do
  @moduledoc """
	  A sample Scheduler example
  """
  use GenServer
  alias MyCoolApp.Scheduler

  def start_link(next_cron \\ 60_000, start_cron \\ 10_000) do
    GenServer.start_link(__MODULE__, %{next_cron: next_cron, start_cron: start_cron})
  end

  def init(data) do
    schedule_initial_job(data.start_cron)
    {:ok, data}
  end

  def handle_info(:execute_job, state) do
    MyJob.perform()
    schedule_next_job(state.next_cron)
    {:noreply, state}
  end

  defp schedule_initial_job(time) do
    Process.send_after(self(), :execute_job, time) 
  end

  defp schedule_next_job(time) do
    Process.send_after(self(), :execute_job, time) 
  end
end

Why have two different functions?
First and foremost maybe you want to schedule the next job at 1-2 hours difference. Yet if the process where to crash before that it wouldn't get run. So by scheduling it in 10 seconds we can be sure it will run upon start.

You can handle as many jobs as you want.

Extra Reading

There's a great blog post written at Dockyard on how to achieve this in a distributed manner.
https://dockyard.com/blog/2017/11/29/need-an-elixir-dependency-to-manage-recurring-jobs-not-so-fast
https://medium.com/@efexen/periodic-tasks-with-elixir-5d9050bcbdb3

Tcl Cron Programming Alternatives

This is something I had devised for a IRC game i had built more than 11 years ago.
When the script is run it runs a procedure newDayCron which runs itself asynchronously every X minutes. I'd usually use it every 30 to 60 minutes but you can even do it every 1 minute.
The following is some code written more than 10 years ago. I've removed the fluff and made it basic and simple.
It can be made better but if you want beauty and function look at the Elixir or PHP versions.

If the script is long running everything will function perfectly. If the script restarts let's say at a computer restart, or the script crashes..
Then it looks to the database to see if not too much time has passed.
This way you're sure to do the task again..

proc newDayCron {nextCron {day 1440}} {
	global LostCon
	set lastday  [dbconn eval {select time from notes where touser='System' and fromuser='SYSTEM'}]
	set read  [dbconn eval {select read from notes where touser='System' and fromuser='SYSTEM'}]
	set newday [clock add $lastday $day minutes]
	set timenow [unixtime]
	#Only run the new day cron IF there is a difference
	if {$newday <= $timenow} {
		#This calculates the days passed since it last run
		set difference [expr {abs($newday-$timenow)/(60*$day)}]
		if {$difference == 0} { set $difference 1 }
		#Make changes in the database
		dbconn eval {UPDATE notes set time=$timenow,read=read+1 WHERE touser='System'}
			if {[expr {$read*$difference}] >= 7} { 
				#If you use 1440 minutes then it runs once a week here..
				#update or run the function multiple times if required and if the 
				#difference is greater than a certain number times
			
			
				dbconn eval {UPDATE notes set read=1 where touser='System'}
			} else {
				#actions to be done once per day/cron issue
			
			 }

	}
	#Run itself again
	after [expr {1000*60*$nextCron}] newDayCron $nextCron $day
}

newDayCron 1440

Conclusion

Cron is still useful in numerous daily implementations. However there are always alternatives which you can implement in your software to cron.
Elixir is a prominent example of how you can leverage the power of the Erlang/OTP platform so you don't require anyother external software.
Whenever possible look at the tools your programming language offers.Integrating everything in the same platform will help you avoid downtime.

Cover Photo: Giallo

Subscribe to my newsletter

NOTE:You will need to confirm your e-mail address in order to fully complete the subscription process.

What are your thoughts?

All comments are moderated and must adhere to the terms of service.

You might enjoy these similar articles: