Using message queues to improve user experience
A major part of the application I develop on my day job is a big DMS, part of which is the ability to distribute documents to staff and external parties. The distribution system works in such a way that, if sending a document to 300 people, there will actually be 300 individual emails created, rather than one email with a list of recipients. This is desired behavior. My problem is, sending 300 emails at the click of a button can take a little time, degrading the experience for the users. This portion of the call graph shows sending to just five people was taking 1+ seconds.
To solve this, I started out to implement a simple queuing system, whereby the distribution requests are added to a queue, before being sent out by a scheduled task.
Rather than refactor a lot of code and do this some fancy way, I quickly put in a solution that proved the concept and seems to work pretty well for now, with minimal effort. As an example (our code’s slightly more complex, I don’t get paid for nothing), here’s what I started with:
< ?php
class EmailSender {
/**
* Takes an array of addresses and sends an email to each one.
*
* @param array $address
*/
public static function sendEmails($address) {
/**
* Code in here
*/
}
}
The idea was fairly simple and I’m sure it’s been done many times before. First step was to rename the existing method and make it private. I then wrote a new method, that checked to see if there was a queue available, if so adds the request to the queue, otherwise calls the old method. Then all I had to do was write a method that checks the queue, running any requests it finds through the original method. We’ve recently adopted the Zend Framework, so checking out Zend_Queue from the incubator, reading some documentation with my docbook goggles on (couldn’t be bothered to build it) and it was pretty much in place.
<?php
/**
* Zend_Queue offline processing hack example
*
* @author Dave Marshall
* @version $Rev: $
* @since $Date: $
* @link $URL: $
*/
class EmailSender {
private static $queue = null;
/**
* Set Queue
*
* @param Zend_Queue $queue
*/
public static function setQueue($queue)
{
self::$queue = $queue;
}
/**
* Takes an array of addresses and sends an email to each one.
*
* @see reallySendEmail
* @see sendQueuedEmails
* @param array $address
*/
public static function sendEmail($address)
{
if (self::$queue === null) {
return self::reallySendEmail($address);
}
self::$queue->send(serialize(func_get_args()));
}
/**
* Takes an array of addresses and sends an email to each one.
*
* @see sendEmail
* @param array $address
*/
private static function reallySendEmail($address)
{
/**
* Code in here
*/
echo 'Sending email to ' . implode(', ', $address) . PHP_EOL;
}
/**
* Reads emails from the queue and sends them
*
* @param int $count - The number of queued items to process
*/
public static function sendQueuedEmails($count)
{
/**
* Should really check the queue is good here
*/
$messages = self::$queue->receive(intval($count));
foreach($messages as $msg) {
$args = unserialize($msg->body);
call_user_func_array(array(__CLASS__, 'reallySendEmail'), $args);
self::$queue->deleteMessage($msg);
}
}
}
set_include_path(
dirname(__FILE__) . '/src/Zend_Framework/library' . PATH_SEPARATOR
. dirname(__FILE__) . '/src/ZendI/library' . PATH_SEPARATOR
. get_include_path()
);
require_once "Zend/Loader.php";
Zend_Loader::registerAutoload();
define('DB_SERVER', 'localhost');
define('DB_PORT', 3306);
define('DB_USER', 'root');
define('DB_PASS', 'password');
define('DB_NAME', 'queue_example');
/**
* Transmittal Queue
*
*/
$config = array(
'name' => 'transmittal',
'driverOptions' => array(
'host' => DB_SERVER,
'port' => DB_PORT,
'username' => DB_USER,
'password' => DB_PASS,
'dbname' => DB_NAME,
'type' => 'pdo_mysql'
)
);
// Create a database queue
$queue = new Zend_Queue('Db', $config);
$queue->createQueue('myqueue'); // called for good measure
EmailSender::setQueue($queue);
/**
* Usage for adding to the queue
*/
EmailSender::sendEmail(array('davemastergeneral@gmail.com'));
/**
* Usage for scheduled task
*/
EmailSender::sendQueuedEmails(5);
This may not be the best practice in the world, but it got the job done Check it out at ZFSnippets.com
About
Dave Marshall is a Software Engineer living near Hull, England. He works on various personal projects and is the Technical Manager at Childcare.co.uk
Dave specialises in web application development for the LAMP stack, but always tries to choose a tool set that is most fit for purpose.
Dave is a Zend Certified Engineer and a Member of the British Computer Soceity.