Typically, server-side scripts that run under a web server have a time-to-live (TTL). When the TTL is exceeded, the script is force-terminated. This is done for the sake of responsiveness and security. Sometimes, you need to start a long-running job in response to a web request. How do we do that without exceeding our TTL?

A common approach is to set up some form of asynchronous job queue — where the script can quickly submit jobs to the queue, then have a separate long-lived process run the jobs. There are many different job queue implementations out there, and some people even roll their own. I use Gearman.

Gearman is not just a job queue: it is “a generic application framework to farm out work to other machines or processes that are better suited to do the work.” So, not only can we queue a job on the same machine. With Gearman, we can queue a job on a different machine, and we can even choose between synchronous and asynchronous execution.

To get started in Ubuntu, Debian, or any other Debian-based distro, first install Gearman and its PHP library:

sudo apt-get install gearman gearman-job-server gearman-tools php5-gearman

In Gearman terminology, we have Workers which handle specific kinds of job request, Clients which submit job requests, and the Gearman Server which routes Client requests to the right Workers.

gearman_worker.php

Here is an example Worker for sending e-mail (since SMTP message submissions are sometimes slow). Start it from the command line with php ./gearman_worker.php. It will listen for incoming jobs until you terminate it with Ctrl + c.

<?php

function fnSendMessageHandler($oGearmanJob) {

   // GET JOB PARAMETERS, THE "WORKLOAD"
   $binWorkload = $oGearmanJob->workload();

   // DE-SERIALIZE INTO AN ASSOCIATIVE ARRAY (2nd PARAM = true)
   $arWork = json_decode($binWorkload, true);

   // SEND THE MESSAGE
   $szFrom = $arWork['from'];
   mail($arWork['to'], $arWork['subject'], $arWork['message'],
        "From: {$szFrom}\r\nReply-to: {$szFrom}");   
}

$oGW = new GearmanWorker();

try {

   // CONNECT TO GEARMAN SERVER
   // NOTE: ALWAYS EXPLICITLY PROVIDE IP/PORT, DEFAULT PARAMS BROKEN IN PHP LIB
   if( !$oGW->addServer('127.0.0.1', 4730) )
      throw new Exception('GearmanWorker::addServer(): ' . $oGW->error());

   // REGISTER WORKER FOR THE FUNCTION 'sendMessage'
   if( !$oGW->addFunction('sendMessage', 'fnSendMessageHandler') )
      throw new Exception('GearmanWorker::addFunction(): ' . $oGW->error());

   // WAIT FOR SUBMITTED 'sendMessage' JOBS
   echo "STARTING WORK...\n";
   while( $oGW->work() );

   $retCode = $oGW->returnCode();

   if( $retCode !== GEARMAN_SUCCESS )
      throw new Exception('GearmanWorker::returnCode(): ' . $oGW->returnCode());
}
catch( Exception $e ) {

   echo $e->getMessage();
   echo "\n";
   exit(-1);
}

exit(0);
      

gearman_client.php

Here is an example Client for queueing an e-mail message to be sent by the Worker defined above. Note that the call to ->doBackground(...) returns immediately and does not wait for the message to be sent.

<?php

try {

   // SUBMIT TO GEARMAN
   $oGC = new GearmanClient();

   if( !$oGC->setTimeout(1000) )
      throw new Exception('GearmanClient::setTimeout(): ' . $oGC->error());

   // CONNECT TO GEARMAN SERVER
   // NOTE: ALWAYS EXPLICITLY PROVIDE IP/PORT, DEFAULT PARAMS BROKEN IN PHP LIB
   if( !$oGC->addServer('127.0.0.1', 4730) )
      throw new Exception('GearmanClient::addServer(): ' . $oGC->error());

   // MARSHAL THE DATA VIA JSON FOR CROSS-PLATFORM CONSISTENCY
   $jsonWorkload = json_encode([
      'to'      => 'mark@facebook.com',
      'from'    => 'rms@gnu.org',
      'subject' => 'Reasons not to use Facebook',
      'message' => 'I have never had a Facebook
         account. There is a Facebook account called "Richard Stallman",
         but it is an impostor.',
   ]);

   // RUN JOB ASYNCHRONOUSLY IN BACKGROUND
   $hJob = $oGC->doBackground("sendMessage", $jsonWorkload);

   $retCode = $oGC->returnCode();

   if( $retCode !== GEARMAN_SUCCESS )
      throw new Exception('GearmanClient::returnCode(): ' . $retCode);

}
catch( Exception $e ) {

   echo $e->getMessage();
   echo "\n";
   exit(-1);
}

exit(0);
      

In the PHP documentation for GearmanWorker and GearmanClient, the addServer() method supposedly has default parameters of $host = 127.0.0.1, $port = 4730. As of this writing, there is a bug in the PHP Gearman API. If you rely upon the default parameters — even if you are connecting to 127.0.0.1 on port 4730 — you will get the following connection failure:

PHP Error[2]: GearmanClient::doBackground(): send_packet(GEARMAN_TIMEOUT)
   Failed in receiving() -> libgearman/connection.cc:443

To prevent this from happening, always explicitly provide the host and port.


← Older   Newer →

Leave a Reply


<a href="" title="">, <abbr title="">, <acronym title="">, <b>, <blockquote cite="">, <cite>, <code>, <del datetime="">, <em>, <i>, <q cite="">, <s>, <strike>, <strong>