PageRenderTime 45ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/library/Rediska/PubSub/Channel.php

https://bitbucket.org/ilyabazhenov/speakplace
PHP | 563 lines | 307 code | 94 blank | 162 comment | 62 complexity | d1274cc97338a418a6c0c4cb9041d661 MD5 | raw file
  1. <?php
  2. // Require Rediska
  3. require_once dirname(__FILE__) . '/../../Rediska.php';
  4. /**
  5. * Rediska PubSub channel
  6. *
  7. * @author Ivan Shumkov
  8. * @package Rediska
  9. * @subpackage PublishSubscribe
  10. * @version 0.5.7
  11. * @link http://rediska.geometria-lab.net
  12. * @license http://www.opensource.org/licenses/bsd-license.php
  13. */
  14. class Rediska_PubSub_Channel extends Rediska_Options_RediskaInstance implements Iterator, ArrayAccess
  15. {
  16. const SUBSCRIBE = 'subscribe';
  17. const UNSUBSCRIBE = 'unsubscribe';
  18. const MESSAGE = 'message';
  19. /**
  20. * Subscriptions
  21. *
  22. * @var array
  23. */
  24. protected $_subscriptions = array();
  25. /**
  26. * The pool of subscription connections
  27. *
  28. * @var Rediska_PubSub_Connections
  29. */
  30. protected $_connections;
  31. /**
  32. * Timeout in seconds
  33. *
  34. * @var int
  35. */
  36. protected $_timeout;
  37. /**
  38. * The unix timestamp when started getting a message
  39. *
  40. * @var int
  41. */
  42. protected $_timeStart;
  43. /**
  44. * Need start time?
  45. *
  46. * @var boolean
  47. */
  48. protected $_needStart = true;
  49. /**
  50. * Server alias or connection object
  51. *
  52. * @var string|Rediska_Connection
  53. */
  54. protected $_serverAlias;
  55. /**
  56. * Current channel for iterator
  57. *
  58. * @var string
  59. */
  60. protected $_currentChannel;
  61. /**
  62. * Current message for iterator
  63. *
  64. * @var string
  65. */
  66. protected $_currentMessage;
  67. /**
  68. * Message buffer
  69. *
  70. * @var array
  71. */
  72. static protected $_messages = array();
  73. /**
  74. * Exception class name for options
  75. *
  76. * @var string
  77. */
  78. protected $_optionsException = 'Rediska_PubSub_Exception';
  79. /**
  80. * Constructor
  81. *
  82. * @param string|array $nameOrNames Channel name or array of names
  83. * @param array[optional] $options Options:
  84. * timeout - Timeout in seconds
  85. * serverAlias - Server alias or connection object
  86. * rediska - Rediska instance name, Rediska object or Rediska options for new instance
  87. */
  88. public function __construct($nameOrNames, $options = array())
  89. {
  90. $this->setOptions($options);
  91. $this->_throwIfNotSupported();
  92. $this->_connections = new Rediska_PubSub_Connections($this);
  93. $this->subscribe($nameOrNames);
  94. }
  95. /**
  96. * Subscribe to channel or channels
  97. *
  98. * @param string|array $channelOrChannels Channel name or names
  99. * @return Rediska_PubSub_Channel
  100. */
  101. public function subscribe($channelOrChannels)
  102. {
  103. if (!is_array($channelOrChannels)) {
  104. $channels = array($channelOrChannels);
  105. } else {
  106. $channels = $channelOrChannels;
  107. }
  108. $this->_execCommand(self::SUBSCRIBE, $channels);
  109. return $this;
  110. }
  111. /**
  112. * Unsubscribe from channel or channels
  113. * If $keys is passed - we unsubscribe from all channels
  114. *
  115. * @param string|array[optional] $channelOrChannels
  116. * @return Rediska_PubSub_Channel
  117. */
  118. public function unsubscribe($channelOrChannels = null)
  119. {
  120. if (is_null($channelOrChannels)) {
  121. $channels = $this->_subscriptions;
  122. } elseif (!is_array($channelOrChannels)) {
  123. $channels = array($channelOrChannels);
  124. } else {
  125. $channels = $channelOrChannels;
  126. }
  127. $this->_execCommand(self::UNSUBSCRIBE, $channels);
  128. return $this;
  129. }
  130. /**
  131. * Publish a message to channel
  132. *
  133. * @param $message
  134. * @return int
  135. */
  136. public function publish($message)
  137. {
  138. $rediska = $this->getRediska();
  139. if ($this->getServerAlias() !== null) {
  140. $rediska = $rediska->on($this->getServerAlias());
  141. }
  142. return $rediska->publish($this->_subscriptions, $message);
  143. }
  144. /**
  145. * Has subscriptions?
  146. *
  147. * @return boolean
  148. */
  149. public function hasSubscriptions()
  150. {
  151. return !empty($this->_subscriptions);
  152. }
  153. /**
  154. * Get subscriptions
  155. *
  156. * @return array
  157. */
  158. public function getSubscriptions()
  159. {
  160. return $this->_subscriptions;
  161. }
  162. /**
  163. * Get message
  164. *
  165. * @param integer[optional] Timeout
  166. * @return Rediska_PubSub_Response_Message|null
  167. */
  168. public function getMessage($timeout = null)
  169. {
  170. // Get default timeout
  171. if (!$timeout && $this->getTimeout()) {
  172. $timeout = $this->getTimeout();
  173. }
  174. // Start timer if not started from iterator
  175. if ($timeout && $this->_needStart) {
  176. $this->_timeStart = time();
  177. }
  178. // Get message from connections
  179. while (true) {
  180. // Try to get message from buffer
  181. if (!empty(self::$_messages)) {
  182. /* @var $connection Rediska_Connection */
  183. foreach($this->_connections as $connection) {
  184. $channels = $this->_connections->getChannelsByConnection($connection);
  185. foreach($channels as $channel) {
  186. $key = "{$connection->getAlias()}-$channel";
  187. if (isset(self::$_messages[$key])) {
  188. $message = array_shift(self::$_messages[$key]);
  189. if (empty(self::$_messages[$key])) {
  190. unset(self::$_messages[$key]);
  191. }
  192. return $message;
  193. }
  194. }
  195. }
  196. }
  197. if (empty($this->_subscriptions)) {
  198. return null;
  199. }
  200. /* @var $connection Rediska_Connection */
  201. foreach ($this->_connections as $connection) {
  202. if ($timeout) {
  203. $timeLeft = ($this->_timeStart + $timeout) - time();
  204. if ($timeLeft <= 0) {
  205. // Reset timeStart if time started from this method
  206. if ($this->_needStart) {
  207. $this->_timeStart = 0;
  208. }
  209. return null;
  210. }
  211. $connection->setReadTimeout($timeLeft);
  212. } else {
  213. $connection->setReadTimeout(600);
  214. }
  215. try {
  216. $response = $this->_getResponseFromConnection($connection);
  217. if ($response === null) {
  218. // Sleep before next connection check
  219. usleep(10000); // 0.01 sec
  220. continue;
  221. }
  222. if (!in_array($response->getChannel(), $this->_subscriptions)) {
  223. $this->_addMessageToBuffer($response);
  224. continue;
  225. }
  226. // Reset timeStart if time started from this method
  227. if ($this->_needStart) {
  228. $this->_timeStart = 0;
  229. }
  230. return $response;
  231. } catch (Rediska_Connection_TimeoutException $e) {
  232. if (!$timeout) {
  233. continue;
  234. }
  235. // Reset timeStart if time started from this method
  236. if ($this->_needStart) {
  237. $this->_timeStart = 0;
  238. }
  239. return null;
  240. }
  241. }
  242. }
  243. }
  244. /**
  245. * Set timeout in seconds
  246. *
  247. * @return Rediska_PubSub_Channel
  248. */
  249. public function setTimeout($timeout)
  250. {
  251. $this->_timeout = (int)$timeout;
  252. return $this;
  253. }
  254. /**
  255. * Get timeout
  256. *
  257. * @return int
  258. */
  259. public function getTimeout()
  260. {
  261. return $this->_timeout;
  262. }
  263. /**
  264. * Set server alias
  265. *
  266. * @param $serverAlias
  267. * @return Rediska_PubSub_Channel
  268. */
  269. public function setServerAlias($serverAlias)
  270. {
  271. $this->_serverAlias = $serverAlias;
  272. return $this;
  273. }
  274. /**
  275. * Get server alias
  276. *
  277. * @return Rediska_Connection|string
  278. */
  279. public function getServerAlias()
  280. {
  281. return $this->_serverAlias;
  282. }
  283. /* Iterator implementation */
  284. public function rewind()
  285. {
  286. if ($this->_timeout) {
  287. $this->_timeStart = time();
  288. }
  289. }
  290. public function next()
  291. {
  292. }
  293. public function valid()
  294. {
  295. $this->_needStart = false;
  296. $message = $this->getMessage();
  297. $this->_needStart = true;
  298. if ($message) {
  299. $this->_currentChannel = $message->getChannel();
  300. $this->_currentMessage = $message->getMessage();
  301. return true;
  302. } else {
  303. $this->_currentChannel = null;
  304. $this->_currentMessage = null;
  305. $this->_timeStart = 0;
  306. return false;
  307. }
  308. }
  309. public function key()
  310. {
  311. return $this->_currentChannel;
  312. }
  313. public function current()
  314. {
  315. return $this->_currentMessage;
  316. }
  317. /* ArrayAccess implementation */
  318. public function offsetSet($offset, $value)
  319. {
  320. if (!is_null($offset)) {
  321. throw new Rediska_PubSub_Exception('Offset is not allowed in Rediska_PubSub_Channel');
  322. }
  323. $this->publish($value);
  324. return $value;
  325. }
  326. public function offsetExists($value)
  327. {
  328. throw new Rediska_PubSub_Exception('Offset is not allowed in Rediska_PubSub_Channel');
  329. }
  330. public function offsetUnset($value)
  331. {
  332. throw new Rediska_PubSub_Exception('Offset is not allowed in Rediska_PubSub_Channel');
  333. }
  334. public function offsetGet($value)
  335. {
  336. throw new Rediska_PubSub_Exception('Offset is not allowed in Rediska_PubSub_Channel');
  337. }
  338. /**
  339. * Execute subscribe or unsubscribe command
  340. *
  341. * @param string $command
  342. * @param array $channels
  343. */
  344. protected function _execCommand($command, $channels)
  345. {
  346. // Group channels by connections
  347. $channelsByConnections = array();
  348. foreach($channels as $channel) {
  349. $hasSubscription = in_array($channel, $this->_subscriptions);
  350. if ($command == self::SUBSCRIBE) {
  351. if ($hasSubscription) {
  352. throw new Rediska_PubSub_Exception("You already subscribed to $channel");
  353. } else {
  354. $this->_subscriptions[] = $channel;
  355. }
  356. }
  357. if ($command == self::UNSUBSCRIBE) {
  358. if (!$hasSubscription) {
  359. throw new Rediska_PubSub_Exception("You not subscribed to $channel");
  360. } else {
  361. $key = array_search($channel, $this->_subscriptions);
  362. unset($this->_subscriptions[$key]);
  363. }
  364. }
  365. $hasChannel = $this->_connections->hasChannel($channel);
  366. if (($command == self::SUBSCRIBE && !$hasChannel) || ($command == self::UNSUBSCRIBE && $hasChannel)) {
  367. switch ($command) {
  368. case self::SUBSCRIBE:
  369. $connection = $this->_connections->addChannel($channel);
  370. break;
  371. case self::UNSUBSCRIBE:
  372. $connection = $this->_connections->removeChannel($channel);
  373. break;
  374. }
  375. $connectionAlias = $connection->getAlias();
  376. if (!array_key_exists($connectionAlias, $channelsByConnections)) {
  377. $channelsByConnections[$connectionAlias] = array();
  378. }
  379. $channelsByConnections[$connectionAlias][] = $channel;
  380. }
  381. }
  382. // Write commands to connections
  383. foreach($channelsByConnections as $connectionAlias => $channels) {
  384. $execCommand = array($command);
  385. foreach($channels as $channel) {
  386. $execCommand[] = $this->getRediska()->getOption('namespace') . $channel;
  387. }
  388. $connection = $this->_connections->getConnectionByAlias($connectionAlias);
  389. $exec = new Rediska_Connection_Exec($connection, $execCommand);
  390. $exec->write();
  391. }
  392. // Get responses from connections
  393. while (!empty($channelsByConnections)) {
  394. foreach ($channelsByConnections as $connectionAlias => $channels) {
  395. $connection = $this->_connections->getConnectionByAlias($connectionAlias);
  396. foreach($channels as $channel) {
  397. $response = $this->_getResponseFromConnection($connection);
  398. // TODO: May be timeout? Not data or server die()
  399. if ($response === null) {
  400. continue;
  401. }
  402. $channel = $response->getChannel();
  403. if (($command == self::SUBSCRIBE && $response instanceof Rediska_PubSub_Response_Subscribe)
  404. || ($command == self::UNSUBSCRIBE && $response instanceof Rediska_PubSub_Response_Unsubscribe)) {
  405. $key = array_search($channel, $channelsByConnections[$connectionAlias]);
  406. unset($channelsByConnections[$connectionAlias][$key]);
  407. if (empty($channelsByConnections[$connectionAlias])) {
  408. unset($channelsByConnections[$connectionAlias]);
  409. }
  410. } else if ($response instanceof Rediska_PubSub_Response_Message) {
  411. $this->_addMessageToBuffer($response);
  412. }
  413. }
  414. }
  415. }
  416. }
  417. /**
  418. * Add message response to buffer
  419. *
  420. * @param Rediska_PubSub_Response_Message $message
  421. */
  422. protected function _addMessageToBuffer(Rediska_PubSub_Response_Message $message)
  423. {
  424. $key = "{$message->getConnection()->getAlias()}-{$message->getChannel()}";
  425. if (!isset(self::$_messages[$key])) {
  426. self::$_messages[$key] = array();
  427. }
  428. self::$_messages[$key][] = $message;
  429. }
  430. /**
  431. * Get response from connection
  432. *
  433. * @param Rediska_Connection $connection
  434. * @return Rediska_PubSub_Response_Abstract
  435. */
  436. protected function _getResponseFromConnection(Rediska_Connection $connection)
  437. {
  438. $response = Rediska_Connection_Exec::readResponseFromConnection($connection);
  439. if ($response === null || $response === true) {
  440. return null;
  441. }
  442. list($type, $channel, $body) = $response;
  443. if ($this->getRediska()->getOption('namespace') !== '' && strpos($channel, $this->getRediska()->getOption('namespace')) === 0) {
  444. $channel = substr($channel, strlen($this->getRediska()->getOption('namespace')));
  445. }
  446. switch ($type) {
  447. case self::SUBSCRIBE:
  448. return new Rediska_PubSub_Response_Subscribe($connection, $channel);
  449. case self::UNSUBSCRIBE:
  450. return new Rediska_PubSub_Response_Unsubscribe($connection, $channel);
  451. case self::MESSAGE:
  452. $message = $this->getRediska()->getSerializer()->unserialize($body);
  453. return new Rediska_PubSub_Response_Message($connection, $channel, $message);
  454. default:
  455. throw new Rediska_PubSub_Response_Exception('Unknown reponse type: ' . $type);
  456. }
  457. }
  458. /**
  459. * Throw if PubSub not supported by Redis
  460. */
  461. protected function _throwIfNotSupported()
  462. {
  463. $version = '1.3.8';
  464. $redisVersion = $this->getRediska()->getOption('redisVersion');
  465. if (version_compare($version, $this->getRediska()->getOption('redisVersion')) == 1) {
  466. throw new Rediska_PubSub_Exception("Publish/Subscribe requires {$version}+ version of Redis server. Current version is {$redisVersion}. To change it specify 'redisVersion' option.");
  467. }
  468. }
  469. }