What is a cron? and How can we use it in web development? are the most common questions people ask. I am going to answer these questions and discuss issues you might encounter.

Glossary

  • A cron is a programme which executes programmes according to schedule. It is a time-based job scheduler.
  • A cron task is a programme being executed by cron.
  • A crontab is a file which contains the schedule of cron tasks to be run and at specified times.

Setting up a cron

Crontab commands

  • crontab -e – Edit crontab file, or create one if it doesn’t already exist.
  • crontab -l – Crontab list of cron jobs, display crontab file contents.
  • crontab -r – Remove your crontab file.

Crontab syntax

*     *     *   *    *        command to be executed
-     -     -   -    -
|     |     |   |    |
|     |     |   |    +----- day of week (0 - 6) (Sunday=0)
|     |     |   +------- month (1 - 12)
|     |     +--------- day of month (1 - 31)
|     +----------- hour (0 - 23)
+------------- min (0 - 59)

Even though the syntax is simple it is much better to use Online Crontab Expression Editor, especially for beginners.

Write a script

It can be basically anything executable on a given system (Bash, Python, PHP, Ruby, C , etc.). It should behave like a normal programme – return zero on success and return non-zero value otherwise.

Use case

Let’s say we are building a simple mailing service [1]. A task is to send emails to lots of people (~1000) at once eg. newsletter, special offer.

An obvious solution - loop

An obvious solution is to loop through all email addresses and send all emails on the push of the button.

for ($emails in $recipient) {
  $this->sendMail($sender, $recipient, $subject, $content);
}

However, this solution has a flaw – too many emails are being sent during a short period of time. Mail servers do not like that, especially, if you are using the basic ones build in the programming language. It also takes a very long time.

Cron solution

We can create a simple queue of emails using a database:

CREATE TABLE `email` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `sender` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `recipient` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `subject` longtext COLLATE utf8_unicode_ci NOT NULL,
  `content` longtext COLLATE utf8_unicode_ci NOT NULL,
  `created` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

Such queue contains mails which are to be sent. Once they are sent, we delete them. Sending script might look like this using Nette and Symfony Console:

<?php

namespace EmailModule\Console;

use Doctrine\ORM\ORMInvalidArgumentException;
use EmailModule\Entity\Email;
use EmailModule\Facade\EmailFacade;
use Nette\InvalidStateException;
use Nette\Mail\Message;
use Nette\Mail\SendmailMailer;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Class EmailCommand sends at most 50 oldest emails from email queue.
 *
 * @package EmailModule\Console
 */
class EmailCommand extends Command
{
    /**
     * Command configuration.
     */
    protected function configure()
    {
        $this->setName('app:email')
            ->setDescription('Sends enqueued emails');
    }

    /**
     * Command execution routine.
     *
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return int return status
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        /** @var EmailFacade $emailFacade */
        $emailFacade = $this->getHelper('container')->getByType('EmailModule\Facade\EmailFacade');
        $emails = $emailFacade->getEmailsToSend();

        $sentCount = 0;
        $responseStatus = 0;

        // Try to send all emails
        foreach ($emails as $email) {
            /** @var Email $email */

            $mail = new Message();
            $mail->setFrom($email->getSender());
            $mail->addTo($email->getRecipient());
            $mail->setSubject($email->getSubject());
            $mail->setHtmlBody($email->getContent());

            $mailer = new SendmailMailer;

            try {
                $mailer->send($mail);
                $sentCount++;
                $emailFacade->dequeueEmail($email);
            } catch (InvalidStateException $e) {
                $output->writeLn('<error>' . $e->getMessage() . '</error>');
                $responseStatus = 1;
            } catch (ORMInvalidArgumentException $e) {
                $output->writeLn('<error>' . $e->getMessage() . '</error>');
                $responseStatus = 1;
            }
        }

        $output->writeLn("");
        $output->writeLn($sentCount .  "/" . count($emails). ' emails sent.');

        return $responseStatus;
    }
}

It loops through emails and tries to send them. Now we add a cron task. We want to execute the task with some reasonable period. For example, every 15 minutes send up to 50 emails, that means at most 200 emails per hour (Feel free to modify these numbers to suit your use case). Crontab entry looks like this:

*/15 * * * * /home/www/mail_service/app/console app:email

Pitfalls

  • Cron task might fail.
  • Crontab entry might not be set (programmer forgot).
  • Crontab entry might be set incorrectly (typing error)
  • Crontab must end with empty line

Conclusion

As you can see, setting up a cron task is simple. First, you write a programme in your favourite language, then you figure out when it should be executed and then you set the cron task up.


Sources

Footnotes

  1. Actually, it is not a very good idea to create a mail sending service like this. To know why read 5 subtle ways you’re using MySQL as a queue, and why it’ll bite you. It is much better to use message queue like RabbitMQ