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