├── docgen ├── logo.png ├── gen.sh └── footer.tpl ├── testscenario ├── celeryconfig.py ├── README.md └── tasks.py ├── install_amqp.sh ├── amqplibconnectorssl.php ├── composer.json ├── unittest ├── CeleryPECLTest.php ├── CeleryAMQPLibTest.php └── unittest.php ├── test.php ├── README.md ├── amqppeclconnector.php ├── amqp.php ├── amqplibconnector.php └── celery.php /docgen/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DivideAndConquer/celery-php/master/docgen/logo.png -------------------------------------------------------------------------------- /testscenario/celeryconfig.py: -------------------------------------------------------------------------------- 1 | BROKER_URL = "amqp://gdr:test@localhost:5672/wutka" 2 | 3 | CELERY_RESULT_BACKEND = "amqp" 4 | 5 | CELERY_IMPORTS = ("tasks", ) 6 | 7 | CELERY_RESULT_SERIALIZER = "json" 8 | CELERY_TASK_RESULT_EXPIRES = None 9 | -------------------------------------------------------------------------------- /docgen/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd /home/gdr/celery-php 4 | git pull 5 | 6 | phpdoc --title "PHP client for Celery task queue" -o HTML:Smarty:HandS -f /home/gdr/celery-php/celery.php -t /srv/celery-php-doc --sourcecode on 7 | cp docgen/logo.png /srv/celery-php-doc/media/ 8 | -------------------------------------------------------------------------------- /testscenario/README.md: -------------------------------------------------------------------------------- 1 | ## Setting up 2 | 3 | rabbitmqctl add_user gdr test 4 | rabbitmqctl add_vhost wutka 5 | rabbitmqctl set_permissions -p wutka gdr ".*" ".*" ".*" 6 | 7 | ## Running 8 | 9 | cd testscenario 10 | celery worker -l DEBUG -c 20 11 | # In another terminal 12 | phpunit Tests 13 | -------------------------------------------------------------------------------- /testscenario/tasks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from celery.task import task 3 | import time 4 | 5 | @task 6 | def add(x, y): 7 | print "Got add()" 8 | return x + y 9 | 10 | @task 11 | def add_delayed(x, y): 12 | print "Got add_delayed()" 13 | time.sleep(1) 14 | print "Woke up from add_delayed()" 15 | return x+y 16 | 17 | @task 18 | def fail(): 19 | print "Got fail()" 20 | return fffffff 21 | 22 | @task 23 | def delayed(): 24 | print "Got delayed()" 25 | time.sleep(2) 26 | print "Woke up from delayed()" 27 | return 'Woke up' 28 | -------------------------------------------------------------------------------- /install_amqp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Based on http://www.php.net/manual/en/amqp.installation.php 4 | # 5 | # Required packages (debian): 6 | sudo apt-get install git php5-dev make gcc autoconf pkg-config 7 | 8 | # Install librabbitmq-c 9 | git clone https://github.com/alanxz/rabbitmq-c.git 10 | cd rabbitmq-c 11 | git submodule init 12 | git submodule update 13 | autoreconf -i && ./configure && make && sudo make install 14 | 15 | # Install and compile PHP extension 16 | sudo pecl install amqp 17 | 18 | # Problem? 19 | # Make sure php.ini is loading amqp.so 20 | # Test with php -i | grep amqp 21 | -------------------------------------------------------------------------------- /amqplibconnectorssl.php: -------------------------------------------------------------------------------- 1 | =2.0.0" 19 | }, 20 | "autoload": { 21 | "classmap": ["celery.php"] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docgen/footer.tpl: -------------------------------------------------------------------------------- 1 |
2 |
3 | This software has been brought to you by 4 | {php} 5 | $bullshit = array( 6 | 'Massive Scale - fast, scalable web applications', 7 | 'Massive Scale - make your web page faster', 8 | 'Massive Scale - make Joomla run faster', 9 | 'Massive Scale - let us help you debug your applications', 10 | ); 11 | echo $bullshit[array_rand($bullshit)]; 12 | {/php} 13 |
14 | Documentation generated on {$date} by phpDocumentor {$phpdocversion} 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /unittest/CeleryPECLTest.php: -------------------------------------------------------------------------------- 1 | PostTask('tasks.add', array(2,2)); 41 | $result = $c->PostTask('tasks.delayed', array()); 42 | #$result = $c->PostTask('tasks.fail', array()); 43 | #echo $result; 44 | 45 | while(!$result->isReady()) 46 | { 47 | # sleep(1); 48 | echo '...'; 49 | } 50 | 51 | if($result->isSuccess()) 52 | { 53 | echo $result->getResult(); 54 | } 55 | else 56 | { 57 | echo "ERROR"; 58 | echo $result->getTraceback(); 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PHP client capable of executing [Celery](http://celeryproject.org/) tasks and reading asynchronous results. 2 | 3 | Uses [AMQP extension from PECL](http://www.php.net/manual/en/amqp.setup.php) or the [PHP AMQP implementation](https://github.com/videlalvaro/php-amqplib) and the following settings in Celery: 4 | 5 | CELERY_RESULT_SERIALIZER = "json" 6 | CELERY_TASK_RESULT_EXPIRES = None 7 | 8 | PECL-AMQP is supported in version 1.0.0 and higher because its API has been completely remade when it entered 1.0. 9 | There is a separate branch for 0.3. 10 | 11 | Last PHP-amqplib version tested is 2.2.6. 12 | 13 | Last Celery version tested is 3.1.11 14 | 15 | ## POSTING TASKS 16 | 17 | $c = new Celery('localhost', 'myuser', 'mypass', 'myvhost'); 18 | $result = $c->PostTask('tasks.add', array(2,2)); 19 | 20 | ## CONNECTING VIA SSL 21 | Connecting to a RabbitMQ server that requires SSL is currently only possible via PHP-amqplib to do so you'll need to 22 | create a celery object with ssl options: 23 | 24 | $ssl_options = array( 25 | 'cafile' => 'PATH_TO_CA_CERT_FILE', 26 | 'verify_peer' => true, 27 | 'passphrase' => 'LOCAL_CERT_PASSPHRASE', 28 | 'local_cert' => 'PATH_TO_COMBINED_CLIENT_CERT_KEY', 29 | 'CN_match' => 'CERT_COMMON_NAME' 30 | ); 31 | 32 | $c = new Celery($host, $user, $password, $vhost, 'celery', 'celery', 5671, false, false, $ssl_options); 33 | 34 | ## READING ASYNC RESULTS 35 | 36 | while(!$result->isReady()) 37 | { 38 | sleep(1); 39 | echo '...'; 40 | } 41 | 42 | if($result->isSuccess()) 43 | { 44 | echo $result->getResult(); 45 | } 46 | else 47 | { 48 | echo "ERROR"; 49 | echo $result->getTraceback(); 50 | } 51 | 52 | ## PYTHON-LIKE API 53 | 54 | An API compatible to AsyncResult in Python is available too. 55 | 56 | $c = new Celery('localhost', 'myuser', 'mypass', 'myvhost'); 57 | $result = $c->PostTask('tasks.add', array(2,2)); 58 | 59 | $result->get(); 60 | if($result->successful()) 61 | { 62 | echo $result->result; 63 | } 64 | 65 | 66 | ## ABOUT 67 | 68 | Based on [this blog post](http://www.toforge.com/2011/01/run-celery-tasks-from-php/) and reading Celery sources. Thanks to Skrat, author of [Celerb](https://github.com/skrat/celerb) for a tip about response encoding. Created for the needs of my consulting work at [Massive Scale](http://massivescale.net/). 69 | License is 2-clause BSD. 70 | 71 | PHP-amqplib support is *experimental* right now. It passes most unit tests and should be safe to work with, though. 72 | 73 | ## SUPPORT 74 | 75 | If you need help integrating Celery in your PHP app, you may be interested in hiring me as a [consultant](http://massivescale.net/performance-for-developers.html). 76 | -------------------------------------------------------------------------------- /amqppeclconnector.php: -------------------------------------------------------------------------------- 1 | setHost($details['host']); 21 | $connection->setLogin($details['login']); 22 | $connection->setPassword($details['password']); 23 | $connection->setVhost($details['vhost']); 24 | $connection->setPort($details['port']); 25 | 26 | return $connection; 27 | } 28 | 29 | /** 30 | * Initialize connection on a given connection object 31 | * @return NULL 32 | */ 33 | function Connect($connection) 34 | { 35 | $connection->connect(); 36 | } 37 | 38 | /** 39 | * Post a task to exchange specified in $details 40 | * @param AMQPConnection $connection Connection object 41 | * @param array $details Array of connection details 42 | * @param string $task JSON-encoded task 43 | * @param array $params AMQP message parameters 44 | */ 45 | function PostToExchange($connection, $details, $task, $params) 46 | { 47 | $ch = new AMQPChannel($connection); 48 | $xchg = new AMQPExchange($ch); 49 | $xchg->setName($details['exchange']); 50 | 51 | $success = $xchg->publish($task, $details['binding'], 0, $params); 52 | $connection->disconnect(); 53 | 54 | return $success; 55 | } 56 | 57 | /** 58 | * Return result of task execution for $task_id 59 | * @param AMQPConnection $connection Connection object 60 | * @param string $task_id Celery task identifier 61 | * @return array array('body' => JSON-encoded message body, 'complete_result' => AMQPEnvelope object) 62 | * or false if result not ready yet 63 | */ 64 | function GetMessageBody($connection, $task_id) 65 | { 66 | $this->Connect($connection); 67 | $ch = new AMQPChannel($connection); 68 | $q = new AMQPQueue($ch); 69 | $q->setName($task_id); 70 | $q->setFlags(AMQP_AUTODELETE | AMQP_DURABLE); 71 | $q->declare(); 72 | try 73 | { 74 | $q->bind('celeryresults', $task_id); 75 | } 76 | catch(AMQPQueueException $e) 77 | { 78 | $q->delete(); 79 | $connection->disconnect(); 80 | return false; 81 | } 82 | 83 | $message = $q->get(AMQP_AUTOACK); 84 | 85 | if(!$message) 86 | { 87 | $q->delete(); 88 | $connection->disconnect(); 89 | return false; 90 | } 91 | 92 | if($message->getContentType() != 'application/json') 93 | { 94 | $q->delete(); 95 | $connection->disconnect(); 96 | 97 | throw new CeleryException('Response was not encoded using JSON - found ' . 98 | $message->getContentType(). 99 | ' - check your CELERY_RESULT_SERIALIZER setting!'); 100 | } 101 | 102 | $q->delete(); 103 | $connection->disconnect(); 104 | 105 | return array( 106 | 'complete_result' => $message, 107 | 'body' => $message->getBody(), 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /amqp.php: -------------------------------------------------------------------------------- 1 | JSON-encoded message body, 'complete_result' => library-specific message object) 117 | * or false if result not ready yet 118 | */ 119 | abstract function GetMessageBody($connection, $task_id); 120 | } 121 | 122 | 123 | ?> 124 | -------------------------------------------------------------------------------- /amqplibconnector.php: -------------------------------------------------------------------------------- 1 | channel(); 52 | 53 | $ch->queue_declare( 54 | $details['binding'], /* queue name - "celery" */ 55 | false, /* passive */ 56 | true, /* durable */ 57 | false, /* exclusive */ 58 | false /* auto_delete */ 59 | ); 60 | 61 | $ch->exchange_declare( 62 | $details['exchange'], /* name */ 63 | 'direct', /* type */ 64 | false, /* passive */ 65 | true, /* durable */ 66 | false /* auto_delete */ 67 | ); 68 | 69 | $ch->queue_bind( 70 | $details['binding'], /* queue name - "celery" */ 71 | $details['exchange'] /* exchange name - "celery" */ 72 | ); 73 | 74 | $msg = new AMQPMessage( 75 | $task, 76 | $params 77 | ); 78 | 79 | $ch->basic_publish($msg, $details['exchange']); 80 | 81 | $ch->close(); 82 | 83 | /* Satisfy Celery::PostTask() error checking */ 84 | /* TODO: catch some exceptions? Which ones? */ 85 | return TRUE; 86 | } 87 | 88 | /** 89 | * A callback function for AMQPChannel::basic_consume 90 | * @param PhpAmqpLib\Message\AMQPMessage $msg 91 | */ 92 | function Consume($msg) 93 | { 94 | $this->message = $msg; 95 | } 96 | 97 | /** 98 | * Return result of task execution for $task_id 99 | * @param object $connection AMQPConnection object 100 | * @param string $task_id Celery task identifier 101 | * @return array array('body' => JSON-encoded message body, 'complete_result' => AMQPMessage object) 102 | * or false if result not ready yet 103 | */ 104 | function GetMessageBody($connection, $task_id) 105 | { 106 | if(!$this->receiving_channel) 107 | { 108 | $ch = $connection->channel(); 109 | 110 | $ch->queue_declare( 111 | $task_id, /* queue name */ 112 | false, /* passive */ 113 | true, /* durable */ 114 | false, /* exclusive */ 115 | true /* auto_delete */ 116 | ); 117 | 118 | $ch->queue_bind($task_id, 'celeryresults'); 119 | 120 | $ch->basic_consume( 121 | $task_id, /* queue */ 122 | '', /* consumer tag */ 123 | false, /* no_local */ 124 | false, /* no_ack */ 125 | false, /* exclusive */ 126 | false, /* nowait */ 127 | array($this, 'Consume') /* callback */ 128 | ); 129 | $this->receiving_channel = $ch; 130 | } 131 | 132 | try 133 | { 134 | $this->receiving_channel->wait(null, false, $this->wait_timeout); 135 | } 136 | catch(PhpAmqpLib\Exception\AMQPTimeoutException $e) 137 | { 138 | return false; 139 | } 140 | 141 | /* Check if the callback function saved something */ 142 | if($this->message) 143 | { 144 | $this->receiving_channel->queue_delete($task_id); 145 | $this->receiving_channel->close(); 146 | $connection->close(); 147 | 148 | return array( 149 | 'complete_result' => $this->message, 150 | 'body' => $this->message->body, // JSON message body 151 | ); 152 | } 153 | 154 | return false; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /unittest/unittest.php: -------------------------------------------------------------------------------- 1 | get_c(); 60 | 61 | $c->PostTask('task.test', 'arg'); 62 | } 63 | 64 | public function testCorrectOperation() 65 | { 66 | $c = $this->get_c(); 67 | 68 | $result = $c->PostTask('tasks.add', array(2,2)); 69 | 70 | for($i = 0; $i < 10; $i++) 71 | { 72 | if($result->isReady()) 73 | { 74 | break; 75 | } 76 | else 77 | { 78 | sleep(1); 79 | } 80 | } 81 | $this->assertTrue($result->isReady()); 82 | 83 | $this->assertTrue($result->isSuccess()); 84 | $this->assertEquals(4, $result->getResult()); 85 | } 86 | 87 | public function testFailingOperation() 88 | { 89 | $c = $this->get_c(); 90 | 91 | $result = $c->PostTask('tasks.fail', array()); 92 | 93 | for($i = 0; $i < 20; $i++) 94 | { 95 | if($result->isReady()) 96 | { 97 | break; 98 | } 99 | else 100 | { 101 | sleep(1); 102 | } 103 | } 104 | $this->assertTrue($result->isReady()); 105 | 106 | $this->assertFalse($result->isSuccess()); 107 | $this->assertGreaterThan(1, strlen($result->getTraceback())); 108 | } 109 | 110 | /** 111 | * @expectedException CeleryException 112 | */ 113 | public function testPrematureGet() 114 | { 115 | $c = $this->get_c(); 116 | 117 | $result = $c->PostTask('tasks.delayed', array()); 118 | $result->isSuccess(); 119 | } 120 | 121 | /** 122 | * @expectedException CeleryException 123 | */ 124 | public function testPrematureGetTraceback() 125 | { 126 | $c = $this->get_c(); 127 | 128 | $result = $c->PostTask('tasks.delayed', array()); 129 | $result->getTraceback(); 130 | } 131 | 132 | /** 133 | * @expectedException CeleryException 134 | */ 135 | public function testPrematureGetResult() 136 | { 137 | $c = $this->get_c(); 138 | 139 | $result = $c->PostTask('tasks.delayed', array()); 140 | $result->getResult(); 141 | } 142 | 143 | public function testFailed() 144 | { 145 | $c = $this->get_c(); 146 | 147 | $result = $c->PostTask('tasks.fail', array()); 148 | $result->get(); 149 | $this->assertTrue($result->failed()); 150 | } 151 | 152 | /* 153 | * Test Python API 154 | * Based on http://www.celeryproject.org/tutorials/first-steps-with-celery/ 155 | */ 156 | public function testGet() 157 | { 158 | $c = $this->get_c(); 159 | 160 | $result = $c->PostTask('tasks.add_delayed', array(4,4)); 161 | # $this->assertFalse($result->ready()); // TODO uncomment when this happens https://github.com/videlalvaro/php-amqplib/pull/80 162 | # $this->assertNull($result->result); // TODO uncomment when this happens https://github.com/videlalvaro/php-amqplib/pull/80 163 | $rv = $result->get(); 164 | $this->assertEquals(8, $rv); 165 | $this->assertEquals(8, $result->result); 166 | $this->assertTrue($result->successful()); 167 | } 168 | 169 | public function testKwargs() 170 | { 171 | $c = $this->get_c(); 172 | 173 | $result = $c->PostTask('tasks.add_delayed', array('x' => 4, 'y' => 4)); 174 | # $this->assertFalse($result->ready()); // TODO uncomment when this happens https://github.com/videlalvaro/php-amqplib/pull/80 175 | # $this->assertNull($result->result); // TODO uncomment when this happens https://github.com/videlalvaro/php-amqplib/pull/80 176 | $rv = $result->get(); 177 | $this->assertEquals(8, $rv); 178 | $this->assertEquals(8, $result->result); 179 | $this->assertTrue($result->successful()); 180 | } 181 | 182 | /** 183 | * @expectedException CeleryTimeoutException 184 | */ 185 | public function testzzzzGetTimeLimit() 186 | { 187 | $c = $this->get_c(); 188 | 189 | $result = $c->PostTask('tasks.delayed', array()); 190 | $result->get(1, TRUE, 0.1); 191 | } 192 | 193 | public function testStateProperty() 194 | { 195 | $c = $this->get_c(); 196 | 197 | $result = $c->PostTask('tasks.delayed', array()); 198 | $this->assertEquals($result->state, 'PENDING'); 199 | $result->get(); 200 | $this->assertEquals($result->state, 'SUCCESS'); 201 | } 202 | 203 | /* NO-OP functions should not fail */ 204 | public function testForget() 205 | { 206 | $c = $this->get_c(); 207 | 208 | $result = $c->PostTask('tasks.add', array(2,2)); 209 | $result->forget(); 210 | $result->revoke(); 211 | } 212 | 213 | public function testWait() 214 | { 215 | $c = $this->get_c(); 216 | 217 | $result = $c->PostTask('tasks.add', array(4,4)); 218 | $rv = $result->wait(); 219 | $this->assertEquals(8, $rv); 220 | $this->assertEquals(8, $result->result); 221 | $this->assertTrue($result->successful()); 222 | } 223 | 224 | public function testSerialization() 225 | { 226 | $c = $this->get_c(); 227 | 228 | $result_tmp = $c->PostTask('tasks.add_delayed', array(4,4)); 229 | $result_serialized = serialize($result_tmp); 230 | $result = unserialize($result_serialized); 231 | $rv = $result->get(); 232 | $this->assertEquals(8, $rv); 233 | $this->assertEquals(8, $result->result); 234 | $this->assertTrue($result->successful()); 235 | } 236 | } 237 | 238 | -------------------------------------------------------------------------------- /celery.php: -------------------------------------------------------------------------------- 1 | 42 | */ 43 | 44 | /** 45 | * General exception class 46 | * @package celery-php 47 | */ 48 | class CeleryException extends Exception {}; 49 | /** 50 | * Emited by AsyncResult::get() on timeout 51 | * @package celery-php 52 | */ 53 | class CeleryTimeoutException extends CeleryException {}; 54 | class CeleryPublishException extends CeleryException {}; 55 | 56 | require('amqp.php'); 57 | 58 | /** 59 | * Client for a Celery server 60 | * @package celery-php 61 | */ 62 | class Celery 63 | { 64 | private $connection = null; // AMQPConnection object 65 | private $connection_details = array(); // array of strings required to connect 66 | private $amqp = null; // AbstractAMQPConnector implementation 67 | 68 | function __construct($host, $login, $password, $vhost, $exchange='celery', $binding='celery', $port=5672, $connector=false, $persistent_messages=false, $ssl_options = array() ) 69 | { 70 | $ssl = !empty($ssl_options); 71 | foreach(array('host', 'login', 'password', 'vhost', 'exchange', 'binding', 'port', 'connector', 'persistent_messages', 'ssl_options') as $detail) 72 | { 73 | $this->connection_details[$detail] = $$detail; 74 | } 75 | 76 | if($connector === false) 77 | { 78 | $this->connection_details['connector'] = AbstractAMQPConnector::GetBestInstalledExtensionName($ssl); 79 | } 80 | $this->amqp = AbstractAMQPConnector::GetConcrete($this->connection_details['connector']); 81 | 82 | $this->connection = self::InitializeAMQPConnection($this->connection_details); 83 | 84 | $this->amqp->Connect($this->connection); 85 | } 86 | 87 | static function InitializeAMQPConnection($details) 88 | { 89 | $amqp = AbstractAMQPConnector::GetConcrete($details['connector']); 90 | return $amqp->GetConnectionObject($details); 91 | } 92 | 93 | /** 94 | * Post a task to Celery 95 | * @param string $task Name of the task, prefixed with module name (like tasks.add for function add() in task.py) 96 | * @param array $args Array of arguments (kwargs call when $args is associative) 97 | * @return AsyncResult 98 | */ 99 | function PostTask($task, $args, $async_result=true) 100 | { 101 | if(!is_array($args)) 102 | { 103 | throw new CeleryException("Args should be an array"); 104 | } 105 | $id = uniqid('php_', TRUE); 106 | 107 | /* $args is numeric -> positional args */ 108 | if(array_keys($args) === range(0, count($args) - 1)) 109 | { 110 | $kwargs = array(); 111 | } 112 | /* $args is associative -> contains kwargs */ 113 | else 114 | { 115 | $kwargs = $args; 116 | $args = array(); 117 | } 118 | 119 | $task_array = array( 120 | 'id' => $id, 121 | 'task' => $task, 122 | 'args' => $args, 123 | 'kwargs' => (object)$kwargs, 124 | ); 125 | $task = json_encode($task_array); 126 | $params = array('content_type' => 'application/json', 127 | 'content_encoding' => 'UTF-8', 128 | 'immediate' => false, 129 | ); 130 | 131 | if($this->connection_details['persistent_messages']) 132 | { 133 | $params['delivery_mode'] = 2; 134 | } 135 | 136 | $success = $this->amqp->PostToExchange( 137 | $this->connection, 138 | $this->connection_details, 139 | $task, 140 | $params 141 | ); 142 | 143 | if(!$success) 144 | { 145 | throw new CeleryPublishException(); 146 | } 147 | 148 | if($async_result) 149 | { 150 | return new AsyncResult($id, $this->connection_details, $task_array['task'], $args); 151 | } 152 | else 153 | { 154 | return true; 155 | } 156 | } 157 | } 158 | 159 | /* 160 | * Asynchronous result of Celery task 161 | * @package celery-php 162 | */ 163 | class AsyncResult 164 | { 165 | private $task_id; // string, queue name 166 | private $connection; // AMQPConnection instance 167 | private $connection_details; // array of strings required to connect 168 | private $complete_result; // Backend-dependent message instance (AMQPEnvelope or PhpAmqpLib\Message\AMQPMessage) 169 | private $body; // decoded array with message body (whatever Celery task returned) 170 | private $amqp = null; // AbstractAMQPConnector implementation 171 | 172 | /** 173 | * Don't instantiate AsyncResult yourself, used internally only 174 | * @param string $id Task ID in Celery 175 | * @param array $connection_details used to initialize AMQPConnection, keys are the same as args to Celery::__construct 176 | * @param string task_name 177 | * @param array task_args 178 | */ 179 | function __construct($id, $connection_details, $task_name=NULL, $task_args=NULL) 180 | { 181 | $this->task_id = $id; 182 | $this->connection = Celery::InitializeAMQPConnection($connection_details); 183 | $this->connection_details = $connection_details; 184 | $this->task_name = $task_name; 185 | $this->task_args = $task_args; 186 | $this->amqp = AbstractAMQPConnector::GetConcrete($connection_details['connector']); 187 | } 188 | 189 | function __wakeup() 190 | { 191 | if($this->connection_details) 192 | { 193 | $this->connection = Celery::InitializeAMQPConnection($this->connection_details); 194 | } 195 | } 196 | 197 | /** 198 | * Connect to queue, see if there's a result waiting for us 199 | * Private - to be used internally 200 | */ 201 | private function getCompleteResult() 202 | { 203 | if($this->complete_result) 204 | { 205 | return $this->complete_result; 206 | } 207 | 208 | $message = $this->amqp->GetMessageBody($this->connection, $this->task_id); 209 | 210 | if($message !== false) 211 | { 212 | $this->complete_result = $message['complete_result']; 213 | $this->body = json_decode( 214 | $message['body'] 215 | ); 216 | } 217 | 218 | return false; 219 | } 220 | 221 | /** 222 | * Helper function to return current microseconds time as float 223 | */ 224 | static private function getmicrotime() 225 | { 226 | list($usec, $sec) = explode(" ",microtime()); 227 | return ((float)$usec + (float)$sec); 228 | } 229 | 230 | /** 231 | * Get the Task Id 232 | * @return string 233 | */ 234 | function getId() 235 | { 236 | return $this->task_id; 237 | } 238 | 239 | /** 240 | * Check if a task result is ready 241 | * @return bool 242 | */ 243 | function isReady() 244 | { 245 | return ($this->getCompleteResult() !== false); 246 | } 247 | 248 | /** 249 | * Return task status (needs to be called after isReady() returned true) 250 | * @return string 'SUCCESS', 'FAILURE' etc - see Celery source 251 | */ 252 | function getStatus() 253 | { 254 | if(!$this->body) 255 | { 256 | throw new CeleryException('Called getStatus before task was ready'); 257 | } 258 | return $this->body->status; 259 | } 260 | 261 | /** 262 | * Check if task execution has been successful or resulted in an error 263 | * @return bool 264 | */ 265 | function isSuccess() 266 | { 267 | return($this->getStatus() == 'SUCCESS'); 268 | } 269 | 270 | /** 271 | * If task execution wasn't successful, return a Python traceback 272 | * @return string 273 | */ 274 | function getTraceback() 275 | { 276 | if(!$this->body) 277 | { 278 | throw new CeleryException('Called getTraceback before task was ready'); 279 | } 280 | return $this->body->traceback; 281 | } 282 | 283 | /** 284 | * Return a result of successful execution. 285 | * In case of failure, this returns an exception object 286 | * @return mixed Whatever the task returned 287 | */ 288 | function getResult() 289 | { 290 | if(!$this->body) 291 | { 292 | throw new CeleryException('Called getResult before task was ready'); 293 | } 294 | 295 | return $this->body->result; 296 | } 297 | 298 | /**************************************************************************** 299 | * Python API emulation * 300 | * http://ask.github.com/celery/reference/celery.result.html * 301 | ****************************************************************************/ 302 | 303 | /** 304 | * Returns TRUE if the task failed 305 | */ 306 | function failed() 307 | { 308 | return $this->isReady() && !$this->isSuccess(); 309 | } 310 | 311 | /** 312 | * Forget about (and possibly remove the result of) this task 313 | * Currently does nothing in PHP client 314 | */ 315 | function forget() 316 | { 317 | } 318 | 319 | /** 320 | * Wait until task is ready, and return its result. 321 | * @param float $timeout How long to wait, in seconds, before the operation times out 322 | * @param bool $propagate (TODO - not working) Re-raise exception if the task failed. 323 | * @param float $interval Time to wait (in seconds) before retrying to retrieve the result 324 | * @throws CeleryTimeoutException on timeout 325 | * @return mixed result on both success and failure 326 | */ 327 | function get($timeout=10, $propagate=TRUE, $interval=0.5) 328 | { 329 | /** 330 | * This is an ugly workaround for PHP-AMQPLIB lack of support for fractional wait time 331 | * @TODO remove the whole 'if' when php-amqp accepts https://github.com/videlalvaro/php-amqplib/pull/80 332 | */ 333 | $original_interval = $interval; 334 | if(property_exists($this->connection, 'wait_timeout')) 335 | { 336 | if($this->connection->wait_timeout < $interval) 337 | { 338 | $interval = 0; 339 | } 340 | else 341 | { 342 | $interval -= $this->connection->wait_timeout; 343 | } 344 | } 345 | 346 | $interval_us = (int)($interval * 1000000); 347 | $iteration_limit = ceil($timeout / $original_interval); 348 | 349 | $start_time = self::getmicrotime(); 350 | while(self::getmicrotime() - $start_time < $timeout) 351 | { 352 | if($this->isReady()) 353 | { 354 | break; 355 | } 356 | 357 | usleep($interval_us); 358 | } 359 | 360 | if(!$this->isReady()) 361 | { 362 | throw new CeleryTimeoutException(sprintf('AMQP task %s(%s) did not return after %d seconds', $this->task_name, json_encode($this->task_args), $timeout), 4); 363 | } 364 | 365 | return $this->getResult(); 366 | } 367 | 368 | /** 369 | * Implementation of Python's properties: result, state/status 370 | */ 371 | public function __get($property) 372 | { 373 | /** 374 | * When the task has been executed, this contains the return value. 375 | * If the task raised an exception, this will be the exception instance. 376 | */ 377 | if($property == 'result') 378 | { 379 | if($this->isReady()) 380 | { 381 | return $this->getResult(); 382 | } 383 | else 384 | { 385 | return NULL; 386 | } 387 | } 388 | /** 389 | * state: The tasks current state. 390 | * 391 | * Possible values includes: 392 | * 393 | * PENDING 394 | * The task is waiting for execution. 395 | * 396 | * STARTED 397 | * The task has been started. 398 | * 399 | * RETRY 400 | * The task is to be retried, possibly because of failure. 401 | * 402 | * FAILURE 403 | * The task raised an exception, or has exceeded the retry limit. The result attribute then contains the exception raised by the task. 404 | * 405 | * SUCCESS 406 | * The task executed successfully. The result attribute then contains the tasks return value. 407 | * 408 | * status: Deprecated alias of state. 409 | */ 410 | elseif($property == 'state' || $property == 'status') 411 | { 412 | if($this->isReady()) 413 | { 414 | return $this->getStatus(); 415 | } 416 | else 417 | { 418 | return 'PENDING'; 419 | } 420 | } 421 | 422 | return $this->$property; 423 | } 424 | 425 | /** 426 | * Returns True if the task has been executed. 427 | * If the task is still running, pending, or is waiting for retry then False is returned. 428 | */ 429 | function ready() 430 | { 431 | return $this->isReady(); 432 | } 433 | 434 | /** 435 | * Send revoke signal to all workers 436 | * Does nothing in PHP client 437 | */ 438 | function revoke() 439 | { 440 | } 441 | 442 | /** 443 | * Returns True if the task executed successfully. 444 | */ 445 | function successful() 446 | { 447 | return $this->isSuccess(); 448 | } 449 | 450 | /** 451 | * Deprecated alias to get() 452 | */ 453 | function wait($timeout=10, $propagate=TRUE, $interval=0.5) 454 | { 455 | return $this->get($timeout, $propagate, $interval); 456 | } 457 | } 458 | 459 | --------------------------------------------------------------------------------