PageRenderTime 71ms CodeModel.GetById 32ms RepoModel.GetById 1ms app.codeStats 0ms

/demos/consumer.php

https://github.com/BraveSirRobin/amqphp
PHP | 406 lines | 275 code | 63 blank | 68 comment | 37 complexity | 9c392684ccf7fd516f7c96ee7e37805d MD5 | raw file
  1. <?php
  2. /**
  3. *
  4. * Copyright (C) 2010, 2011, 2012 Robin Harvey (harvey.robin@gmail.com)
  5. *
  6. * This library is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public License
  8. * as published by the Free Software Foundation; either version 2.1 of
  9. * the License, or (at your option) any later version.
  10. * This library is distributed in the hope that it will be useful, but
  11. * WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13. * Lesser General Public License for more details.
  14. * You should have received a copy of the GNU Lesser General Public
  15. * License along with this library; if not, write to the Free Software
  16. * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  17. * 02110-1301 USA
  18. */
  19. /**
  20. * This demo shows off the use of exit strategies, including using
  21. * combinations of strategies.
  22. */
  23. use amqphp as amqp;
  24. use amqphp\protocol;
  25. use amqphp\wire;
  26. require __DIR__ . '/class-loader.php';
  27. define("DEFAULT_CONF", realpath(__DIR__ . '/configs/basic-connection.xml'));
  28. if (! is_file(DEFAULT_CONF)) {
  29. warn("Fatal: cannnot find connection config %s\n", DEFAULT_CONF);
  30. die;
  31. }
  32. /** Demo implementation class, is both a consumer and a channel event
  33. * handler. */
  34. class MultiConsumer implements amqp\Consumer, amqp\ChannelEventHandler
  35. {
  36. /** CLI Exit strategy identifier map */
  37. public static $StratMap = array(
  38. 'cond' => amqp\Connection::STRAT_COND,
  39. 'trel' => amqp\Connection::STRAT_TIMEOUT_REL,
  40. 'tabs' => amqp\Connection::STRAT_TIMEOUT_ABS,
  41. 'maxl' => amqp\Connection::STRAT_MAXLOOPS,
  42. 'callb' => amqp\Connection::STRAT_CALLBACK
  43. );
  44. /* Specify default 'action messages' (these are defined in CLI args) */
  45. public $exitMessage = 'Cancel.';
  46. public $rejectMessage = 'Reject.';
  47. public $dropMessage = 'Drop.';
  48. private $connection;
  49. private $channel;
  50. private $evl;
  51. private $consumeTags = array();
  52. private $signalsInstalled = false;
  53. function __construct ($config) {
  54. // Set up connection using a Factory
  55. $fact = new amqp\Factory($config);
  56. $tmp = $fact->getConnections();
  57. $this->connection = reset($tmp);
  58. $tmp = $this->connection->getChannels();
  59. $this->channel = reset($tmp);
  60. }
  61. /** Add an exit strategy as defined on the command line */
  62. function addExitStrategy ($strat) {
  63. $bits = explode(' ', $strat);
  64. if (! array_key_exists($bits[0], self::$StratMap)) {
  65. throw new \Exception("Invalid strategy identifier: {$bits[0]}", 34678);
  66. }
  67. $bits[0] = self::$StratMap[$bits[0]];
  68. call_user_func_array(array($this->connection, 'pushExitStrategy'), $bits);
  69. }
  70. /**
  71. * Register a consume session with the local channel and add the
  72. * given consume params to the local stack - these will be picked
  73. * up when the local connection is added to an event loop.
  74. */
  75. function addConsumeSession ($queue, $noLocal=false, $noAck=false, $exclusive=false) {
  76. $consumeParams = array('queue' => $queue,
  77. 'no-local' => $noLocal,
  78. 'no-ack' => $noAck,
  79. 'exclusive' => $exclusive,
  80. 'no-wait' => false);
  81. $this->channel->addConsumer($this, $consumeParams);
  82. }
  83. /**
  84. * Starts a consume session.
  85. */
  86. function runDemo () {
  87. info("Start consuming...");
  88. $this->testEnableSignalHandler();
  89. $this->evl = new amqp\EventLoop;
  90. $this->evl->addConnection($this->connection);
  91. $this->evl->select();
  92. $this->channel->removeAllConsumers();
  93. $this->connection->shutdown();
  94. info("Consumers removed, event loop exits.");
  95. }
  96. /**
  97. * Checks to see if signal handler funcs are available (i.e. not
  98. * Windows or Apache); if they are, installs handlers.
  99. */
  100. private function testEnableSignalHandler () {
  101. if (! $this->signalsInstalled && extension_loaded('pcntl')) {
  102. pcntl_signal(SIGTERM, array($this, 'sigHandler'));
  103. pcntl_signal(SIGHUP, array($this, 'sigHandler'));
  104. pcntl_signal(SIGINT, array($this, 'sigHandler'));
  105. $this->signalsInstalled = true;
  106. info("Signal handler funcs installed OK");
  107. }
  108. }
  109. /** Callback for signal handlers. */
  110. function sigHandler ($signo) {
  111. info("RECEIVED SIGNAL %d, force event loop exit", $signo);
  112. $this->evl->forceLoopExit();
  113. }
  114. /** @override \amqphp\Consumer */
  115. function handleCancelOk (wire\Method $m, amqp\Channel $chan) {
  116. // Remove this consume tag from local list
  117. $cTag = $m->getField('consumer-tag');
  118. $cNum = array_search($cTag, $this->consumeTags);
  119. if ($cNum === false) {
  120. warn("Received a cancel for an unknown consumer tag %s", $cTag);
  121. } else {
  122. info("Consumer %s [%s] cancelled OK", $cNum, $cTag);
  123. }
  124. }
  125. /** @override \amqphp\Consumer */
  126. function handleConsumeOk (wire\Method $m, amqp\Channel $chan) {
  127. $this->consumeTags[] = $m->getField('consumer-tag');
  128. info("Consume session started, ctag %s", $m->getField('consumer-tag'));
  129. }
  130. /** @override \amqphp\Consumer */
  131. function handleDelivery (wire\Method $m, amqp\Channel $chan) {
  132. // Look up which consumer is being delivered to.
  133. $cTag = $m->getField('consumer-tag');
  134. $cNum = array_search($cTag, $this->consumeTags);
  135. if ($cNum === false) {
  136. // This should never happen!
  137. warn("Received message for unknown consume tag %s, reject", $cTag);
  138. return amqp\Connection::CONSUMER_REJECT;
  139. }
  140. $content = $m->getContent();
  141. if (! $content) {
  142. info("Empty Message received on consumer %d [%s]", $cNum, $cTag);
  143. return amqp\Connection::CONSUMER_ACK;
  144. }
  145. if ($content === $this->exitMessage) {
  146. info("Received exit message, cancel consumer %d", $cNum);
  147. return array(amqp\Connection::CONSUMER_ACK, amqp\Connection::CONSUMER_CANCEL);
  148. } else if ($content === $this->rejectMessage) {
  149. info("Received reject message, reject consumer %d", $cNum);
  150. return amqp\Connection::CONSUMER_REJECT;
  151. } else if ($content === $this->dropMessage) {
  152. info("Received drop message, drop consumer %d", $cNum);
  153. return amqp\Connection::CONSUMER_DROP;
  154. } else {
  155. printf("[MSG] consumer-tag=%s [%d]\ndelivery-tag=%s redelivered=%s\nexchange=%s\nrouting-key=%s\n%s\n",
  156. $cTag, $cNum, $m->getField('delivery-tag'), $m->getField('redelivered') ? 't' : 'f', $m->getField('exchange'),
  157. $m->getField('routing-key'), $content);
  158. return amqp\Connection::CONSUMER_ACK;
  159. }
  160. }
  161. /** @override \amqphp\Consumer */
  162. function handleRecoveryOk (wire\Method $m, amqp\Channel $chan) { }
  163. /**
  164. * Called by the API to look for consume session parameters.
  165. * We're providing these via. the addConnection method rather than
  166. * here, although we could over-ride those params here
  167. * @override \amqphp\Consumer
  168. */
  169. function getConsumeMethod (amqp\Channel $chan) { }
  170. /** @override \amqphp\ChannelEventHandler */
  171. public function publishConfirm (wire\Method $m) { }
  172. /** @override \amqphp\ChannelEventHandler */
  173. public function publishReturn (wire\Method $m) {
  174. info("Your message was rejected: %s [%d]\n", $m->getField('reply-text'), $m->getField('reply-code'));
  175. $this->requests--;
  176. }
  177. // Server has cancelled us for some reason.
  178. function handleCancel (wire\Method $meth, amqp\Channel $chan) {
  179. $cTag = $meth->getField('consumer-tag');
  180. $cNum = array_search($cTag, $this->consumeTags);
  181. if ($cNum === false) {
  182. // This should never happen!
  183. warn("Received cancellation for unknown consumer tag %s, reject", $cTag);
  184. } else {
  185. info("Received a consumer cancel from broker for consumer %d [%s]", $cNum, $cTag);
  186. }
  187. }
  188. /** @override \amqphp\ChannelEventHandler */
  189. public function publishNack (wire\Method $m) { }
  190. }
  191. /**
  192. * Use or replace this to test the callback exit strategy
  193. */
  194. function randomExitController () {
  195. $r = ((rand(0,25) % 25) != 0);
  196. info("Random exit controller invoked, returns %d", $r);
  197. return $r;
  198. }
  199. $USAGE = sprintf('Usage: php consumer.php [switches]
  200. Starts a consume session and prints received messages to the command
  201. line. The consume parameters, exit strategies and other items can be
  202. configured with the following switches:
  203. --config [file-path] Load connection configs from this file, default
  204. %s
  205. --strat ["name args"] - Adds a strategy to the connection strategy
  206. chain, you can specify multiple strategies
  207. name: {%s}
  208. args: Optional whitespace separated list of strategy parameters
  209. You can specify multiple strategies using more than one --strat
  210. option.
  211. --consumer ["queue consume-args"] Adds a consumer
  212. queue: Name of the queue to listen on
  213. consume-args: 3 consumer setup flags, must be a sequence of 3
  214. t/f values corresponding to the following consumer setup
  215. properties, with the following defaults:
  216. no-local: f
  217. no-ack: f
  218. exclusive: f
  219. (Note: RabbitMQ does not support the no-local flag:
  220. http://www.rabbitmq.com/interoperability.html)
  221. --exit-message [string] - when the following message string is
  222. received, exit the receiving consumer with
  223. Connection::CONSUMER_CANCEL. Default Value: "Cancel."
  224. --reject-message [string] - when the following message string is
  225. received, reject the incoming message with
  226. Connection::CONSUMER_REJECT. Default Value: "Reject." (Note:
  227. RabbitMQ does not support the no-local flag:
  228. http://www.rabbitmq.com/interoperability.html)
  229. --drop-message [string] - when the following message string is
  230. received, reject the incoming message with
  231. Connection::CONSUMER_DROP. Default Value: "Drop."
  232. You can add more than one consumer by using more than one
  233. --consumer option.
  234. Example:
  235. This should work "out of the box":
  236. php consumer.php --strat "cond" \
  237. --strat "trel 5 0" \
  238. --consumer "most-basic-q"
  239. ',
  240. DEFAULT_CONF,
  241. implode(', ', array_keys(MultiConsumer::$StratMap)));
  242. /** Some output functions to write messages to stdout. */
  243. function info () {
  244. $args = func_get_args();
  245. if (! $fmt = array_shift($args)) {
  246. return;
  247. }
  248. $fmt = sprintf("[INFO] %s\n", $fmt);
  249. vprintf($fmt, $args);
  250. }
  251. function warn() {
  252. $args = func_get_args();
  253. if (! $fmt = array_shift($args)) {
  254. return;
  255. }
  256. $fmt = sprintf("[WARN] %s\n", $fmt);
  257. vprintf($fmt, $args);
  258. }
  259. /** Create the demo client and configure it as per CLI args. */
  260. $opts = getopt('', array('help', 'strat:', 'consumer:', 'exit-message:', 'reject-message:', 'drop-message:', 'config:'));
  261. if (array_key_exists('help', $opts)) {
  262. echo $USAGE;
  263. die;
  264. }
  265. if (array_key_exists('config', $opts)) {
  266. if (is_array($opts['config'])) {
  267. warn("Too many config options, discarding all but the first.");
  268. $opts['config'] = array_shift($opts['config']);
  269. }
  270. } else {
  271. $opts['config'] = DEFAULT_CONF;
  272. }
  273. // Load consumers from the command line args
  274. $consumeSessions = array();
  275. if (! array_key_exists('consumer', $opts)) {
  276. printf("Error: you must specify at least one --consumer option\n");
  277. die;
  278. }
  279. foreach ((array) $opts['consumer'] as $cOpt) {
  280. $bits = explode(' ', $cOpt);
  281. $queue = $bits[0];
  282. $cFlags = array_key_exists(1, $bits)
  283. ? $bits[1]
  284. : 'fff';
  285. if (strlen($cFlags) != 3) {
  286. print("Error: invalid consumer switch, consume params option must contain exactly 3 characters.\n");
  287. die;
  288. }
  289. $consumeSessions[] = array($queue,
  290. $cFlags[0] == 't',
  291. $cFlags[1] == 't',
  292. $cFlags[2] == 't');
  293. }
  294. info("Start demo from config %s", $opts['config']);
  295. $exd = new \MultiConsumer($opts['config']);
  296. // Apply exit strategies from the command line
  297. if (array_key_exists('strat', $opts)) {
  298. foreach ((array) $opts['strat'] as $strat) {
  299. $exd->addExitStrategy($strat);
  300. info("Added exit strategy %s", $strat);
  301. }
  302. }
  303. // Create consumers
  304. foreach ($consumeSessions as $cs) {
  305. info("Add consume session: queue=%s, no-local=%s, no-ack=%s, exclusive=%s",
  306. $cs[0],
  307. ($cs[1] ? 't' : 'f'),
  308. ($cs[2] ? 't' : 'f'),
  309. ($cs[3] ? 't' : 'f'));
  310. call_user_func_array(array($exd, 'addConsumeSession'), $cs);
  311. }
  312. if (array_key_exists('exit-message', $opts)) {
  313. $exd->exitMessage = $opts['exit-message'];
  314. }
  315. if (array_key_exists('reject-message', $opts)) {
  316. $exd->rejectMessage = $opts['reject-message'];
  317. }
  318. if (array_key_exists('drop-message', $opts)) {
  319. $exd->dropMessage = $opts['drop-message'];
  320. }
  321. try {
  322. $exd->runDemo();
  323. } catch (\Exception $e) {
  324. printf("Exception caught at root level of consumer script:\n%s\n%s",
  325. $e->getMessage(), $e->getTraceAsString());
  326. }