Skip to end of metadata
Go to start of metadata

<h1>Zend Queue Refactoring</h1>

<p>The ZendQueue is currently in a bad state. The DB implementation is based on Zend\Db from one of the previous Beta releases and it's at least not functional at all. Same with "PlatformJobQueue" - Zend Job Queue adapter implementation needs a complete rewrite. And the coding style looks very mixed - from php4 to php5, some oop code, some common interface, many strings that interpreted to cause different object generations at runtime and so on (but not on an OOP way such as factories).</p>

<p>The adapter's choice itself is also not really up 2 date - I'm missing at least Beanstalkd and maybe Gearman (should be discussed). Maybe you should think about removing MemcacheQ - it's not maintained anymore and shouldn't be used in an enterprise architecture anyways. But doesn't hurt anyone to keep it in there, it's simple ...</p>

<p>I'd love to take over the refactoring for this component. Maybe it will be the best to rewrite this component completly (but this will be of course up 2 you). If you want to, I can come up with a prototype on github that follows my suggestions below. You can review that and I'll do the "details". </p>

<h2>Component Overview</h2>
<p>The ZendQueue component itself is very straight forward. There are only a few logical subcomponents:</p>
<ol>
<li><strong>Zend Queue</strong><br />
The "interface to the user" - you're working most of the time on this component</li>
<li><strong>Message</strong><br />
The message that is sent to the queue or retrieved by the queue</li>
<li><strong>Adapter</strong><br />
The implementation itself for the specific technologies (based on an AbstractAdapter)</li>
<li><strong>Exception</strong><br />
A big bunch of custom exceptions are delivered by this component</li>
</ol>

<p>The following adapters are implemented:</p>
<ol>
<li>Activemq (+ Stomp)</li>
<li>Memcacheq</li>
<li>Null</li>
<li>Array</li>
<li>DB (+ AbstractTableGateways and sql's)</li>
<li>PlatformJobQueue (has to be ZendJobQueue)</li>
</ol>

<h2>Architecture overview</h2>
<h4>ZendQueue</h4>
<p>The class \ZendQueue\Queue has following dependencies (within this project):</p>
<ul>
<li>\ZendQueue\Message</li>
<li>\ZendQueue\Message\MessageIterator</li>
<li>\ZendQueue\AdapterAbstract</li>
<li>\ZendQueue\Exception\ExceptionInterface</li>
<li>$options - an array or \Zend\Config</li>
</ul>

<p>This component is the "interface to the user" from an API point of view. Most of the actions are performed against this kind of object.<br />
Some methods are used to be a proxy for the adapter implementation. Some of them are only for configuration (with an array or \Zend\Config).</p>

<h4>Adapter</h4>
<p>The class \ZendQueue\AbstractAdapter has the following dependencies (within this project):</p>
<ul>
<li>\ZendQueue\Queue</li>
<li>\ZendQueue\Adapter (internal interface for adapters)</li>
<li>\ZendQueue\Exception\ExceptionInterface</li>
<li>$options - an array or \Zend\Config</li>
</ul>

<p>This class (together with the Adapter interface) is the base for every adapter. As you can see, there is a cross-dependency to the queue.<br />
The whole functionality of the used technology took place in the implementations of this abstract class. Simply extend \ZendQueue\AbstractAdapter, implement the needed methods, overwrite what you need, inject to the queue and work on another queue system.</p>

<h4>Message / MessageList</h4>
<p>The class \ZendQueue\Message has the following dependencies (within this project):</p>
<ul>
<li>\ZendQueue\Exception\ExceptionInterface</li>
<li>\ZendQueue\Queue</li>
</ul>

<p>This class represents a message that is sent to the queue or received from the queue. Magic methods get, set, isset, sleep and wakeup are implemented.<br />
Some helper functions to import an array or build object from an array. </p>

<p>The class \ZendQueue\Message\MessageIterator has the following dependencies (within this project):</p>
<ul>
<li>\ZendQueue\Adapter\AdapterInterface</li>
<li>\ZendQueue\Queue</li>
<li>\ZendQueue\Message</li>
<li>\ZendQueue\Exception\ExceptionInterface</li>
</ul>

<p>This is a Message Container that is returned by the adapter methods. All of the methods that are working with the message and the message iterator are proxied from the queue to the adapter.<br />
So there's no explicit cross dependency between the queue and the message.</p>

<h2>Refactoring suggestions</h2>

<p>Some suggestions about a possible refactoring.<br />
It's not a refactoring by definition due to interface changes - but I think we can stay on this wording <ac:emoticon ac:name="smile" /></p>

<h4>Queue and Adapter dependency</h4>
<p>The queue is used as a proxy to the adapter by some methods:</p>
<ac:macro ac:name="code"><ac:parameter ac:name="title">send (ZendQueueQueue)</ac:parameter><ac:plain-text-body><![CDATA[
/**

  • Send a message to the queue
    *
  • @param mixed $message message
  • @return Message
  • @throws Exception\ExceptionInterface
    */
    public function send($message)
    {
    return $this->getAdapter()->send($message);
    }
    ]]></ac:plain-text-body></ac:macro>

<p>As I said above, there is a cross dependency between the Queue and the Adapter:</p>
<ac:macro ac:name="code"><ac:parameter ac:name="title">send (ZendQueueAdapterActivemq)</ac:parameter><ac:plain-text-body><![CDATA[
/**

  • Push an element onto the end of the queue
    *
  • @param string $message message to send to the queue
  • @param Queue $queue
  • @return Message
    */
    public function send($message, Queue $queue=null)
    {
    if ($queue === null)
    Unknown macro: { $queue = $this->_queue; }

$frame = $this->_client->createFrame();
$frame->setCommand('SEND');
$frame->setHeader('destination', $queue->getName());
$frame->setHeader('content-length', strlen($message));
$frame->setBody((string) $message);
$this->_client->send($frame);

$data = array(
'message_id' => null,
'body' => $message,
'md5' => md5($message),
'handle' => null
);

$options = array(
'queue' => $queue,
'data' => $data
);
$classname = $queue->getMessageClass();
return new $classname($options);
}
]]></ac:plain-text-body></ac:macro>

<p>I see no point, why the queue at all shouldn't be used as a proxy?! If the whole queue is doing nothing else than proxying everything to the adapter, the cross dependency is gone and the queue object can still be used as a "single point of usage" for the end-user. And all the exceptions that are thrown in all of the not implemented methods can be handled at a central place:</p>

<ac:macro ac:name="code"><ac:parameter ac:name="title">isExists (ZendQueueQueue)</ac:parameter><ac:plain-text-body><![CDATA[
public function isExists($message) {
if(!$this->isSupported(_FUNCTION_))
throw new Exception\UnsupportedMethodCallException('isExists is not supported by this adapter');

return $this->getAdapter()->isExists($name);
}
]]></ac:plain-text-body></ac:macro>

<p>And such code like this can be removed completly from the adapter implementation:</p>

<ac:macro ac:name="code"><ac:parameter ac:name="title">isExists (ZendQueueAdapterActivemq)</ac:parameter><ac:plain-text-body><![CDATA[
/**

  • Does a queue already exist?
    *
  • @param string $name
  • @return boolean
  • @throws Exception\UnsupportedMethodCallException (not supported)
    */
    public function isExists($name)
    {
    throw new Exception\UnsupportedMethodCallException('isExists() is not supported in this adapter');
    }
    ]]></ac:plain-text-body></ac:macro>

<h4>isSupported and getCapabilities</h4>

<p>The way how specific adapter functionalities are checked is based on a kind of configuration array. The function getCapabilities (part of \ZendQueue\Adapter\AdapterInterface) is used by isSupported from the Queue to check individual functions, if they are supported by the used adapter or not:</p>
<ac:macro ac:name="code"><ac:parameter ac:name="title">getCapabilities (ZendQueueAdapterActivemq)</ac:parameter><ac:plain-text-body><![CDATA[
/**

  • Return a list of queue capabilities functions
    *
  • $array['function name'] = true or false
  • true is supported, false is not supported.
    *
  • @param string $name
  • @return array
    */
    public function getCapabilities()
    {
    return array(
    'create' => false,
    'delete' => false,
    'send' => true,
    'receive' => true,
    'deleteMessage' => true,
    'getQueues' => false,
    'count' => false,
    'isExists' => false,
    );
    }
    ]]></ac:plain-text-body></ac:macro>

<ac:macro ac:name="code"><ac:parameter ac:name="title">isSupported (ZendQueueAdapterAbstractAdapter)</ac:parameter><ac:plain-text-body><![CDATA[
/**

  • Indicates if a function is supported or not.
    *
  • @param string $name
  • @return boolean
    */
    public function isSupported($name)
    {
    $list = $this->getCapabilities();

return (isset($list[$name]) && $list[$name]);
}
]]></ac:plain-text-body></ac:macro>

<ac:macro ac:name="code"><ac:parameter ac:name="title">isSupported (ZendQueueQueue)</ac:parameter><ac:plain-text-body><![CDATA[
/**

  • Indicates if a function is supported or not.
    *
  • @param string $name
  • @return boolean
    */
    public function isSupported($name)
    {
    $translation = array(
    'deleteQueue' => 'delete',
    'createQueue' => 'create'
    );

if (isset($translation[$name]))

Unknown macro: { $name = $translation[$name]; }

return $this->getAdapter()->isSupported($name);
}
]]></ac:plain-text-body></ac:macro>

<p>I suggest to either use duck typing here to check for the existence of specific methods in the adapters, or develop against feature interfaces.</p>

<h5>Duck typing</h5>

<p>Duck typing will be an easy way but not very object oriented. I suggest to choose this if the number of features is higher than 8 - otherwise the other suggestion will look a bit overengineered.</p>

<ac:macro ac:name="code"><ac:parameter ac:name="title">isSupported (ZendQueueQueue)</ac:parameter><ac:plain-text-body><![CDATA[
public function isSupported($name) {
return method_exists($name);
}
]]></ac:plain-text-body></ac:macro>

<h5>Feature Interfaces</h5>

<p>If the number of features is not that high, I suggest to implement a feature interface such as:</p>
<ac:macro ac:name="code"><ac:parameter ac:name="title">FeatureInterface</ac:parameter><ac:plain-text-body><![CDATA[
namespace ZendQueue\Feature;

interface IsExistsInterface {
public function isExists();
}
]]></ac:plain-text-body></ac:macro>

<ac:macro ac:name="code"><ac:parameter ac:name="title">isSupported (ZendQueueQueue)</ac:parameter><ac:plain-text-body><![CDATA[
public function isSupported($name) {
$interfaceName = "\\ZendQueue\\Feature
".ucfirst($name) . "Interface";

return $this->adapter instanceof $interfaceName;
}
]]></ac:plain-text-body></ac:macro>

<ac:macro ac:name="code"><ac:parameter ac:name="title">Adapter</ac:parameter><ac:plain-text-body><![CDATA[
class Activemq implements IsExistsInterface // ....
{
public function isExists()

Unknown macro: { // .... }

}
]]></ac:plain-text-body></ac:macro>

<p><strong>Note:</strong> If the queue will be changed to a simple proxy, it should implement all provided feature interfaces!</p>

<h4>Component configuration</h4>

<p>I suggest, to use the same way as in \Zend\Cache to configure the instances:</p>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
$cache = StorageFactory::factory(array(
'adapter' => 'apc',
'plugins' => array(
'exception_handler' => array('throw_exceptions' => false),
),
));
]]></ac:plain-text-body></ac:macro>

<p>Become something like this in \ZendQueue:</p>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
$activeMQ = AdapterFactory::factory(array(
'adapter' => 'activemq',
'options' => array(
'scheme' => 'tcp',
'host' => 'localhost',
'port' => 61613
),
));
]]></ac:plain-text-body></ac:macro>

<p>The options array will be the configuration that is passed to something similar than \Zend\Cache\Storage\Adapter\AdapterOptions.<br />
Therefor, you can have a major one for the standard config and an apdapter specific configuration can easily be implemented for the delivered ones and even for custom adapters.</p>

<h4>Component Strategy</h4>

<p>When I'm looking now on the featureset, that is provided by ZendJobQueue and on activemq, the functionality is completly different. You simply want the "basics" in this component provided by most of the queue services or as much features as possible for different adapters? It will not be very easy to develop against interfaces, if the functionality is not even compareable. </p>

<h4>Version support</h4>

<p>What is your strategy if such components rely on specific versions of a service? Memcacheq changed their resultset for "stat" from 0.1.* to 0.2 - it was not compatible anymore. Also activemq switched to a never version of the stomp standard a few years ago. A kind of "VersionFactory" can be easily implemented to have a MemcacheQ Adapter for 0.1 and one for 0.2. Same with activemq - simply the Stomp implementation can be changed. You need that or are you fine with simply having it up 2 date?</p>

<h4>ZF2 integration</h4>

<p>If you want to, I can also come up with a suggestion to integrate this component in ZF2. I only need a bit background about how you'd like to proceed with this component (I think queueing is very common in an enterprise architecture).</p>

<h4>Rewrite of ZendQueue</h4>

<p>The ZendQueue itself has a history for about a few years. You can really see that in the code. Maybe it might be the best to rewrite this component from scratch. Even the sourcecode on the implementation itself looks a bit "old school" to me - definitvly not the way you do it in ZF2. It might be a good idea to change coding style, interface implementations, architecture ... to fit more the way you do it in zf2. </p>

Labels:
rfc rfc Delete
queue queue Delete
refactoring refactoring Delete
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.