Libmemcached.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. <?php
  2. /**
  3. * Zend Framework
  4. *
  5. * LICENSE
  6. *
  7. * This source file is subject to the new BSD license that is bundled
  8. * with this package in the file LICENSE.txt.
  9. * It is also available through the world-wide-web at this URL:
  10. * http://framework.zend.com/license/new-bsd
  11. * If you did not receive a copy of the license and are unable to
  12. * obtain it through the world-wide-web, please send an email
  13. * to license@zend.com so we can send you a copy immediately.
  14. *
  15. * @category Zend
  16. * @package Zend_Cache
  17. * @subpackage Zend_Cache_Backend
  18. * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
  19. * @license http://framework.zend.com/license/new-bsd New BSD License
  20. * @version $Id$
  21. */
  22. /**
  23. * @see Zend_Cache_Backend_Interface
  24. */
  25. require_once 'Zend/Cache/Backend/ExtendedInterface.php';
  26. /**
  27. * @see Zend_Cache_Backend
  28. */
  29. require_once 'Zend/Cache/Backend.php';
  30. /**
  31. * @package Zend_Cache
  32. * @subpackage Zend_Cache_Backend
  33. * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
  34. * @license http://framework.zend.com/license/new-bsd New BSD License
  35. */
  36. class Zend_Cache_Backend_Libmemcached extends Zend_Cache_Backend implements Zend_Cache_Backend_ExtendedInterface
  37. {
  38. /**
  39. * Default Server Values
  40. */
  41. const DEFAULT_HOST = '127.0.0.1';
  42. const DEFAULT_PORT = 11211;
  43. const DEFAULT_WEIGHT = 1;
  44. /**
  45. * Log message
  46. */
  47. const TAGS_UNSUPPORTED_BY_CLEAN_OF_LIBMEMCACHED_BACKEND = 'Zend_Cache_Backend_Libmemcached::clean() : tags are unsupported by the Libmemcached backend';
  48. const TAGS_UNSUPPORTED_BY_SAVE_OF_LIBMEMCACHED_BACKEND = 'Zend_Cache_Backend_Libmemcached::save() : tags are unsupported by the Libmemcached backend';
  49. /**
  50. * Available options
  51. *
  52. * =====> (array) servers :
  53. * an array of memcached server ; each memcached server is described by an associative array :
  54. * 'host' => (string) : the name of the memcached server
  55. * 'port' => (int) : the port of the memcached server
  56. * 'weight' => (int) : number of buckets to create for this server which in turn control its
  57. * probability of it being selected. The probability is relative to the total
  58. * weight of all servers.
  59. * =====> (array) client :
  60. * an array of memcached client options ; the memcached client is described by an associative array :
  61. * @see http://php.net/manual/memcached.constants.php
  62. * - The option name can be the name of the constant without the prefix 'OPT_'
  63. * or the integer value of this option constant
  64. *
  65. * @var array available options
  66. */
  67. protected $_options = array(
  68. 'servers' => array(array(
  69. 'host' => self::DEFAULT_HOST,
  70. 'port' => self::DEFAULT_PORT,
  71. 'weight' => self::DEFAULT_WEIGHT,
  72. )),
  73. 'client' => array()
  74. );
  75. /**
  76. * Memcached object
  77. *
  78. * @var mixed memcached object
  79. */
  80. protected $_memcache = null;
  81. /**
  82. * Constructor
  83. *
  84. * @param array $options associative array of options
  85. * @throws Zend_Cache_Exception
  86. * @return void
  87. */
  88. public function __construct(array $options = array())
  89. {
  90. if (!extension_loaded('memcached')) {
  91. Zend_Cache::throwException('The memcached extension must be loaded for using this backend !');
  92. }
  93. // override default client options
  94. $this->_options['client'] = array(
  95. Memcached::OPT_DISTRIBUTION => Memcached::DISTRIBUTION_CONSISTENT,
  96. Memcached::OPT_HASH => Memcached::HASH_MD5,
  97. Memcached::OPT_LIBKETAMA_COMPATIBLE => true,
  98. );
  99. parent::__construct($options);
  100. if (isset($this->_options['servers'])) {
  101. $value = $this->_options['servers'];
  102. if (isset($value['host'])) {
  103. // in this case, $value seems to be a simple associative array (one server only)
  104. $value = array(0 => $value); // let's transform it into a classical array of associative arrays
  105. }
  106. $this->setOption('servers', $value);
  107. }
  108. $this->_memcache = new Memcached;
  109. // setup memcached client options
  110. foreach ($this->_options['client'] as $name => $value) {
  111. $optId = null;
  112. if (is_int($name)) {
  113. $optId = $name;
  114. } else {
  115. $optConst = 'Memcached::OPT_' . strtoupper($name);
  116. if (defined($optName)) {
  117. $optId = constant($optConst);
  118. }
  119. }
  120. if ($optId) {
  121. $this->_memcache->setOption($optId, $value);
  122. }
  123. }
  124. // setup memcached servers
  125. $servers = array();
  126. foreach ($this->_options['servers'] as $server) {
  127. if (!array_key_exists('port', $server)) {
  128. $server['port'] = self::DEFAULT_PORT;
  129. }
  130. if (!array_key_exists('weight', $server)) {
  131. $server['weight'] = self::DEFAULT_WEIGHT;
  132. }
  133. $servers[] = array($server['host'], $server['port'], $server['weight']);
  134. }
  135. $this->_memcache->addServers($servers);
  136. }
  137. /**
  138. * Test if a cache is available for the given id and (if yes) return it (false else)
  139. *
  140. * @param string $id Cache id
  141. * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested
  142. * @return string|false cached datas
  143. */
  144. public function load($id, $doNotTestCacheValidity = false)
  145. {
  146. $tmp = $this->_memcache->get($id);
  147. if (isset($tmp[0])) {
  148. return $tmp[0];
  149. }
  150. return false;
  151. }
  152. /**
  153. * Test if a cache is available or not (for the given id)
  154. *
  155. * @param string $id Cache id
  156. * @return int|false (a cache is not available) or "last modified" timestamp (int) of the available cache record
  157. */
  158. public function test($id)
  159. {
  160. $tmp = $this->_memcache->get($id);
  161. if (isset($tmp[0], $tmp[1])) {
  162. return (int)$tmp[1];
  163. }
  164. return false;
  165. }
  166. /**
  167. * Save some string datas into a cache record
  168. *
  169. * Note : $data is always "string" (serialization is done by the
  170. * core not by the backend)
  171. *
  172. * @param string $data Datas to cache
  173. * @param string $id Cache id
  174. * @param array $tags Array of strings, the cache record will be tagged by each string entry
  175. * @param int $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime)
  176. * @return boolean True if no problem
  177. */
  178. public function save($data, $id, $tags = array(), $specificLifetime = false)
  179. {
  180. $lifetime = $this->getLifetime($specificLifetime);
  181. // ZF-8856: using set because add needs a second request if item already exists
  182. $result = @$this->_memcache->set($id, array($data, time(), $lifetime), $lifetime);
  183. if ($result === false) {
  184. $rsCode = $this->_memcache->getResultCode();
  185. $rsMsg = $this->_memcache->getResultMessage();
  186. $this->_log("Memcached::set() failed: [{$rsCode}] {$rsMsg}");
  187. }
  188. if (count($tags) > 0) {
  189. $this->_log(self::TAGS_UNSUPPORTED_BY_SAVE_OF_LIBMEMCACHED_BACKEND);
  190. }
  191. return $result;
  192. }
  193. /**
  194. * Remove a cache record
  195. *
  196. * @param string $id Cache id
  197. * @return boolean True if no problem
  198. */
  199. public function remove($id)
  200. {
  201. return $this->_memcache->delete($id);
  202. }
  203. /**
  204. * Clean some cache records
  205. *
  206. * Available modes are :
  207. * 'all' (default) => remove all cache entries ($tags is not used)
  208. * 'old' => unsupported
  209. * 'matchingTag' => unsupported
  210. * 'notMatchingTag' => unsupported
  211. * 'matchingAnyTag' => unsupported
  212. *
  213. * @param string $mode Clean mode
  214. * @param array $tags Array of tags
  215. * @throws Zend_Cache_Exception
  216. * @return boolean True if no problem
  217. */
  218. public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())
  219. {
  220. switch ($mode) {
  221. case Zend_Cache::CLEANING_MODE_ALL:
  222. return $this->_memcache->flush();
  223. break;
  224. case Zend_Cache::CLEANING_MODE_OLD:
  225. $this->_log("Zend_Cache_Backend_Libmemcached::clean() : CLEANING_MODE_OLD is unsupported by the Libmemcached backend");
  226. break;
  227. case Zend_Cache::CLEANING_MODE_MATCHING_TAG:
  228. case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG:
  229. case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG:
  230. $this->_log(self::TAGS_UNSUPPORTED_BY_CLEAN_OF_LIBMEMCACHED_BACKEND);
  231. break;
  232. default:
  233. Zend_Cache::throwException('Invalid mode for clean() method');
  234. break;
  235. }
  236. }
  237. /**
  238. * Return true if the automatic cleaning is available for the backend
  239. *
  240. * @return boolean
  241. */
  242. public function isAutomaticCleaningAvailable()
  243. {
  244. return false;
  245. }
  246. /**
  247. * Set the frontend directives
  248. *
  249. * @param array $directives Assoc of directives
  250. * @throws Zend_Cache_Exception
  251. * @return void
  252. */
  253. public function setDirectives($directives)
  254. {
  255. parent::setDirectives($directives);
  256. $lifetime = $this->getLifetime(false);
  257. if ($lifetime > 2592000) {
  258. // #ZF-3490 : For the memcached backend, there is a lifetime limit of 30 days (2592000 seconds)
  259. $this->_log('memcached backend has a limit of 30 days (2592000 seconds) for the lifetime');
  260. }
  261. if ($lifetime === null) {
  262. // #ZF-4614 : we tranform null to zero to get the maximal lifetime
  263. parent::setDirectives(array('lifetime' => 0));
  264. }
  265. }
  266. /**
  267. * Return an array of stored cache ids
  268. *
  269. * @return array array of stored cache ids (string)
  270. */
  271. public function getIds()
  272. {
  273. $this->_log("Zend_Cache_Backend_Libmemcached::save() : getting the list of cache ids is unsupported by the Libmemcached backend");
  274. return array();
  275. }
  276. /**
  277. * Return an array of stored tags
  278. *
  279. * @return array array of stored tags (string)
  280. */
  281. public function getTags()
  282. {
  283. $this->_log(self::TAGS_UNSUPPORTED_BY_SAVE_OF_LIBMEMCACHED_BACKEND);
  284. return array();
  285. }
  286. /**
  287. * Return an array of stored cache ids which match given tags
  288. *
  289. * In case of multiple tags, a logical AND is made between tags
  290. *
  291. * @param array $tags array of tags
  292. * @return array array of matching cache ids (string)
  293. */
  294. public function getIdsMatchingTags($tags = array())
  295. {
  296. $this->_log(self::TAGS_UNSUPPORTED_BY_SAVE_OF_LIBMEMCACHED_BACKEND);
  297. return array();
  298. }
  299. /**
  300. * Return an array of stored cache ids which don't match given tags
  301. *
  302. * In case of multiple tags, a logical OR is made between tags
  303. *
  304. * @param array $tags array of tags
  305. * @return array array of not matching cache ids (string)
  306. */
  307. public function getIdsNotMatchingTags($tags = array())
  308. {
  309. $this->_log(self::TAGS_UNSUPPORTED_BY_SAVE_OF_LIBMEMCACHED_BACKEND);
  310. return array();
  311. }
  312. /**
  313. * Return an array of stored cache ids which match any given tags
  314. *
  315. * In case of multiple tags, a logical AND is made between tags
  316. *
  317. * @param array $tags array of tags
  318. * @return array array of any matching cache ids (string)
  319. */
  320. public function getIdsMatchingAnyTags($tags = array())
  321. {
  322. $this->_log(self::TAGS_UNSUPPORTED_BY_SAVE_OF_LIBMEMCACHED_BACKEND);
  323. return array();
  324. }
  325. /**
  326. * Return the filling percentage of the backend storage
  327. *
  328. * @throws Zend_Cache_Exception
  329. * @return int integer between 0 and 100
  330. */
  331. public function getFillingPercentage()
  332. {
  333. $mems = $this->_memcache->getStats();
  334. if ($mems === false) {
  335. return 0;
  336. }
  337. $memSize = null;
  338. $memUsed = null;
  339. foreach ($mems as $key => $mem) {
  340. if ($mem === false) {
  341. $this->_log('can\'t get stat from ' . $key);
  342. continue;
  343. }
  344. $eachSize = $mem['limit_maxbytes'];
  345. $eachUsed = $mem['bytes'];
  346. if ($eachUsed > $eachSize) {
  347. $eachUsed = $eachSize;
  348. }
  349. $memSize += $eachSize;
  350. $memUsed += $eachUsed;
  351. }
  352. if ($memSize === null || $memUsed === null) {
  353. Zend_Cache::throwException('Can\'t get filling percentage');
  354. }
  355. return ((int) (100. * ($memUsed / $memSize)));
  356. }
  357. /**
  358. * Return an array of metadatas for the given cache id
  359. *
  360. * The array must include these keys :
  361. * - expire : the expire timestamp
  362. * - tags : a string array of tags
  363. * - mtime : timestamp of last modification time
  364. *
  365. * @param string $id cache id
  366. * @return array array of metadatas (false if the cache id is not found)
  367. */
  368. public function getMetadatas($id)
  369. {
  370. $tmp = $this->_memcache->get($id);
  371. if (isset($tmp[0], $tmp[1], $tmp[2])) {
  372. $data = $tmp[0];
  373. $mtime = $tmp[1];
  374. $lifetime = $tmp[2];
  375. return array(
  376. 'expire' => $mtime + $lifetime,
  377. 'tags' => array(),
  378. 'mtime' => $mtime
  379. );
  380. }
  381. return false;
  382. }
  383. /**
  384. * Give (if possible) an extra lifetime to the given cache id
  385. *
  386. * @param string $id cache id
  387. * @param int $extraLifetime
  388. * @return boolean true if ok
  389. */
  390. public function touch($id, $extraLifetime)
  391. {
  392. $tmp = $this->_memcache->get($id);
  393. if (isset($tmp[0], $tmp[1], $tmp[2])) {
  394. $data = $tmp[0];
  395. $mtime = $tmp[1];
  396. $lifetime = $tmp[2];
  397. $newLifetime = $lifetime - (time() - $mtime) + $extraLifetime;
  398. if ($newLifetime <=0) {
  399. return false;
  400. }
  401. // #ZF-5702 : we try replace() first becase set() seems to be slower
  402. if (!($result = $this->_memcache->replace($id, array($data, time(), $newLifetime), $newLifetime))) {
  403. $result = $this->_memcache->set($id, array($data, time(), $newLifetime), $newLifetime);
  404. if ($result === false) {
  405. $rsCode = $this->_memcache->getResultCode();
  406. $rsMsg = $this->_memcache->getResultMessage();
  407. $this->_log("Memcached::set() failed: [{$rsCode}] {$rsMsg}");
  408. }
  409. }
  410. return $result;
  411. }
  412. return false;
  413. }
  414. /**
  415. * Return an associative array of capabilities (booleans) of the backend
  416. *
  417. * The array must include these keys :
  418. * - automatic_cleaning (is automating cleaning necessary)
  419. * - tags (are tags supported)
  420. * - expired_read (is it possible to read expired cache records
  421. * (for doNotTestCacheValidity option for example))
  422. * - priority does the backend deal with priority when saving
  423. * - infinite_lifetime (is infinite lifetime can work with this backend)
  424. * - get_list (is it possible to get the list of cache ids and the complete list of tags)
  425. *
  426. * @return array associative of with capabilities
  427. */
  428. public function getCapabilities()
  429. {
  430. return array(
  431. 'automatic_cleaning' => false,
  432. 'tags' => false,
  433. 'expired_read' => false,
  434. 'priority' => false,
  435. 'infinite_lifetime' => false,
  436. 'get_list' => false
  437. );
  438. }
  439. }