Dev Blog

Normally, when you use computer, there are numerous background processes which allows you to use them as a services. That is, these processes are always ready to be used. Most obvious examples for web applications are HTTP Server, Database servers - MySQL, PostgreSQL, MongoDB, ElasticSearch or many, many other. When developing web application it is sometimes desirable to have strictly application-related service which cooperates with web application in some way. The use case I plan to describe here is a service that will react on RabbitMQ messages sent by front-end. The messages are generated by using web application and the reason for using message queue is that often these tasks require additional permissions or are time consuming. 

The messages are the kind of triggers for batch processing, which might include:

  • Generating configuration files
  • Dumping database backups
  • Getting SSL certificates

Using CRON

The first idea I came up with was to call PHP script via CRON job. Adding CRON job is very easy and reliable. To minimize delays, the cron job was set to run every minute. While this might be acceptable for long running tasks, for those which should have rather instant effect it is rather long. For instance, when user saves the website, configuration file needed to be generated. But then, our unlucky visitor might need to wait minute until changes will be applied. Actually it is possible to run CRON job more often than once a minute. I've made with sleep command with increasing delays.

Example of CRON job called every 10 seconds

* * * * * /var/www/my-app/cron.sh
* * * * * sleep 10 && /var/www/my-app/cron.sh
* * * * * sleep 20 && /var/www/my-app/cron.sh
* * * * * sleep 30 && /var/www/my-app/cron.sh
* * * * * sleep 40 && /var/www/my-app/cron.sh
* * * * * sleep 50 && /var/www/my-app/cron.sh

Rather ugly hack but might work in your case. There are major flaws in this setup:

  1. The script is executed every 10 seconds no matter if it really should ran or not
  2. The real problem here is the aforementioned  word task. When application requires more than one in-background operation - if not it will require in future, trust me ;) - the CRON setup will be either convoluted or the cron.sh script will have to decide which taks to run
  3. If some long running task will exceed 10 seconds time, another job will start - some locking or queue mechanism is required

RabbitMQ to the rescue

The message queue, in this case RabbitMQ Message Queue Server, will intercept incoming messages and publish them for use by another proces, server or whatever it is connected to it. Using message queue will allow easy adding of new tasks without worrying of anything at all. 

The RabbitMQ setup consists of three parts:

  1. Web application sending messages
  2. RabbitMQ server listening and dispatching messages
  3. PHP process which listens and responds to messages
The message itself can contain payload which can be processed by PHP listener. Setting up PHP to use RabbitMQ is beyond this post, which focuses more on keeping listener running.

Running PHP Script as Service

Running PHP as a service does not sound too good approach because PHP is mainly intended for building web applications. However major advantage is that service can use existing application facilities and code. The process itself is lightweight and most of the time is simply waiting for incoming messages. There are some extra things to keep in mind, especially cleaning up after message is processed. Cleaning up means closing connections to database, close opened files, free memory etc. Operations which should be normally done, but often are ignored because of momentary nature of PHP. When designing PHP tasks which are supposed to be ran on continuously executed script just need more attention to not left garbage behind or memory leaks will occur.

Setting up Service

To create system service we need to create configuration file instructing system of what script should be ran. In Ubuntu for systemd the configuration file is located in /etc/systemd/system/ and should end with .service extension. In the example below we will name it hosting.service. The file contains minimal configuration to be used as service:

[Unit]
Description=Hosting Updating Service

[Service]
User=root
Type=simple
TimeoutSec=0
PIDFile=/var/run/hosting.pid
ExecStart=/bin/bash /var/www/hosting/service
KillMode=process

Restart=on-failure
RestartSec=42s

[Install]
WantedBy=default.target

The configuration is pretty self explanatory, the most important options are:

  • PIDFile - which will create file indicating that the service is up. Just make it unique.
  • ExecStart - command to run. I recommend using bash script to execute PHP script - later on it.

The ExecStart directive must start with /

The script can be symlinked or copied over to /etc/systemd/system/ directory. After modifying script the sudo systemctl daemon-reload command need to be executed to instruct systemd that config was updated.

Startup Script

As mentioned earlier, the PHP is not executed directly, but it is called from inside bash script. This approach is used to setup proper working directory for PHP script and to allow changes without modifying configuration file. The script /var/www/hosting/service is just a PHP launcher:

#!/bin/bash
# This file should be ran as a service by hosting.service
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd $DIR
./maslosoft hosting:service

The first lines are setting up proper working directory, then the PHP script is executed.

Service Install Script

To avoid repeatable manual tasks and make it more robust, service installing script can be created. With the install script, whole code can be shipped along with application. The install script, let's name it install-service.sh contains instructions to setup our script as a daemon:

#!/usr/bin/env bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
file="$DIR/hosting.service"
sudo ln -s "$file" "/etc/systemd/system/hosting.service"
sudo systemctl enable hosting
sudo systemctl start hosting
systemctl status hosting

Similarly to service script, the install-service.sh is setting up proper working directory, then creates symlink to configuration file, enables and starts service and finally displays status. The service installing script can be ran from any directory, as it detects its own location. 

The installing script must be ran with sudo, ie sudo install-service.sh

From now on, if everything went fine, calling systemctl status hosting should display similar output:

● hosting.service - Hosting Updating Service
   Loaded: loaded (/etc/systemd/system/hosting.service; enabled; vendor preset: enabled)
   Active: active (running) since Tue 2019-07-02 13:46:32 CEST; 1 weeks 0 days ago
 Main PID: 13607 (bash)
    Tasks: 2 (limit: 4915)
   CGroup: /system.slice/hosting.service
           ├─13607 /bin/bash /var/www/hosting/service
           └─13623 php ./maslosoft hosting:service

lip 02 13:46:32 server-1 systemd[1]: Started Hosting Updating Service.