PageRenderTime 30ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/cache/stores/redis/lib.php

https://github.com/cmiic/moodle
PHP | 494 lines | 190 code | 44 blank | 260 comment | 20 complexity | db9e6117aa211a599a9f6842d9047875 MD5 | raw file
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Redis Cache Store - Main library
  18. *
  19. * @package cachestore_redis
  20. * @copyright 2013 Adam Durana
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22. */
  23. defined('MOODLE_INTERNAL') || die();
  24. /**
  25. * Redis Cache Store
  26. *
  27. * To allow separation of definitions in Moodle and faster purging, each cache
  28. * is implemented as a Redis hash. That is a trade-off between having functionality of TTL
  29. * and being able to manage many caches in a single redis instance. Given the recommendation
  30. * not to use TTL if at all possible and the benefits of having many stores in Redis using the
  31. * hash configuration, the hash implementation has been used.
  32. *
  33. * @copyright 2013 Adam Durana
  34. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35. */
  36. class cachestore_redis extends cache_store implements cache_is_key_aware, cache_is_lockable,
  37. cache_is_configurable, cache_is_searchable {
  38. /**
  39. * Name of this store.
  40. *
  41. * @var string
  42. */
  43. protected $name;
  44. /**
  45. * The definition hash, used for hash key
  46. *
  47. * @var string
  48. */
  49. protected $hash;
  50. /**
  51. * Flag for readiness!
  52. *
  53. * @var boolean
  54. */
  55. protected $isready = false;
  56. /**
  57. * Cache definition for this store.
  58. *
  59. * @var cache_definition
  60. */
  61. protected $definition = null;
  62. /**
  63. * Connection to Redis for this store.
  64. *
  65. * @var Redis
  66. */
  67. protected $redis;
  68. /**
  69. * Determines if the requirements for this type of store are met.
  70. *
  71. * @return bool
  72. */
  73. public static function are_requirements_met() {
  74. return class_exists('Redis');
  75. }
  76. /**
  77. * Determines if this type of store supports a given mode.
  78. *
  79. * @param int $mode
  80. * @return bool
  81. */
  82. public static function is_supported_mode($mode) {
  83. return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
  84. }
  85. /**
  86. * Get the features of this type of cache store.
  87. *
  88. * @param array $configuration
  89. * @return int
  90. */
  91. public static function get_supported_features(array $configuration = array()) {
  92. return self::SUPPORTS_DATA_GUARANTEE + self::DEREFERENCES_OBJECTS + self::IS_SEARCHABLE;
  93. }
  94. /**
  95. * Get the supported modes of this type of cache store.
  96. *
  97. * @param array $configuration
  98. * @return int
  99. */
  100. public static function get_supported_modes(array $configuration = array()) {
  101. return self::MODE_APPLICATION + self::MODE_SESSION;
  102. }
  103. /**
  104. * Constructs an instance of this type of store.
  105. *
  106. * @param string $name
  107. * @param array $configuration
  108. */
  109. public function __construct($name, array $configuration = array()) {
  110. $this->name = $name;
  111. if (!array_key_exists('server', $configuration) || empty($configuration['server'])) {
  112. return;
  113. }
  114. $prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : '';
  115. $this->redis = $this->new_redis($configuration['server'], $prefix);
  116. }
  117. /**
  118. * Create a new Redis instance and
  119. * connect to the server.
  120. *
  121. * @param string $server The server connection string
  122. * @param string $prefix The key prefix
  123. * @return Redis
  124. */
  125. protected function new_redis($server, $prefix = '') {
  126. $redis = new Redis();
  127. $port = null;
  128. if (strpos($server, ':')) {
  129. $serverconf = explode(':', $server);
  130. $server = $serverconf[0];
  131. $port = $serverconf[1];
  132. }
  133. if ($redis->connect($server, $port)) {
  134. $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
  135. if (!empty($prefix)) {
  136. $redis->setOption(Redis::OPT_PREFIX, $prefix);
  137. }
  138. // Database setting option...
  139. $this->isready = $this->ping($redis);
  140. } else {
  141. $this->isready = false;
  142. }
  143. return $redis;
  144. }
  145. /**
  146. * See if we can ping Redis server
  147. *
  148. * @param Redis $redis
  149. * @return bool
  150. */
  151. protected function ping(Redis $redis) {
  152. try {
  153. if ($redis->ping() === false) {
  154. return false;
  155. }
  156. } catch (Exception $e) {
  157. return false;
  158. }
  159. return true;
  160. }
  161. /**
  162. * Get the name of the store.
  163. *
  164. * @return string
  165. */
  166. public function my_name() {
  167. return $this->name;
  168. }
  169. /**
  170. * Initialize the store.
  171. *
  172. * @param cache_definition $definition
  173. * @return bool
  174. */
  175. public function initialise(cache_definition $definition) {
  176. $this->definition = $definition;
  177. $this->hash = $definition->generate_definition_hash();
  178. return true;
  179. }
  180. /**
  181. * Determine if the store is initialized.
  182. *
  183. * @return bool
  184. */
  185. public function is_initialised() {
  186. return ($this->definition !== null);
  187. }
  188. /**
  189. * Determine if the store is ready for use.
  190. *
  191. * @return bool
  192. */
  193. public function is_ready() {
  194. return $this->isready;
  195. }
  196. /**
  197. * Get the value associated with a given key.
  198. *
  199. * @param string $key The key to get the value of.
  200. * @return mixed The value of the key, or false if there is no value associated with the key.
  201. */
  202. public function get($key) {
  203. return $this->redis->hGet($this->hash, $key);
  204. }
  205. /**
  206. * Get the values associated with a list of keys.
  207. *
  208. * @param array $keys The keys to get the values of.
  209. * @return array An array of the values of the given keys.
  210. */
  211. public function get_many($keys) {
  212. return $this->redis->hMGet($this->hash, $keys);
  213. }
  214. /**
  215. * Set the value of a key.
  216. *
  217. * @param string $key The key to set the value of.
  218. * @param mixed $value The value.
  219. * @return bool True if the operation succeeded, false otherwise.
  220. */
  221. public function set($key, $value) {
  222. return ($this->redis->hSet($this->hash, $key, $value) !== false);
  223. }
  224. /**
  225. * Set the values of many keys.
  226. *
  227. * @param array $keyvaluearray An array of key/value pairs. Each item in the array is an associative array
  228. * with two keys, 'key' and 'value'.
  229. * @return int The number of key/value pairs successfuly set.
  230. */
  231. public function set_many(array $keyvaluearray) {
  232. $pairs = [];
  233. foreach ($keyvaluearray as $pair) {
  234. $pairs[$pair['key']] = $pair['value'];
  235. }
  236. if ($this->redis->hMSet($this->hash, $pairs)) {
  237. return count($pairs);
  238. }
  239. return 0;
  240. }
  241. /**
  242. * Delete the given key.
  243. *
  244. * @param string $key The key to delete.
  245. * @return bool True if the delete operation succeeds, false otherwise.
  246. */
  247. public function delete($key) {
  248. return ($this->redis->hDel($this->hash, $key) > 0);
  249. }
  250. /**
  251. * Delete many keys.
  252. *
  253. * @param array $keys The keys to delete.
  254. * @return int The number of keys successfully deleted.
  255. */
  256. public function delete_many(array $keys) {
  257. // Redis needs the hash as the first argument, so we have to put it at the start of the array.
  258. array_unshift($keys, $this->hash);
  259. return call_user_func_array(array($this->redis, 'hDel'), $keys);
  260. }
  261. /**
  262. * Purges all keys from the store.
  263. *
  264. * @return bool
  265. */
  266. public function purge() {
  267. return ($this->redis->del($this->hash) !== false);
  268. }
  269. /**
  270. * Cleans up after an instance of the store.
  271. */
  272. public function instance_deleted() {
  273. $this->purge();
  274. $this->redis->close();
  275. unset($this->redis);
  276. }
  277. /**
  278. * Determines if the store has a given key.
  279. *
  280. * @see cache_is_key_aware
  281. * @param string $key The key to check for.
  282. * @return bool True if the key exists, false if it does not.
  283. */
  284. public function has($key) {
  285. return $this->redis->hExists($this->hash, $key);
  286. }
  287. /**
  288. * Determines if the store has any of the keys in a list.
  289. *
  290. * @see cache_is_key_aware
  291. * @param array $keys The keys to check for.
  292. * @return bool True if any of the keys are found, false none of the keys are found.
  293. */
  294. public function has_any(array $keys) {
  295. foreach ($keys as $key) {
  296. if ($this->has($key)) {
  297. return true;
  298. }
  299. }
  300. return false;
  301. }
  302. /**
  303. * Determines if the store has all of the keys in a list.
  304. *
  305. * @see cache_is_key_aware
  306. * @param array $keys The keys to check for.
  307. * @return bool True if all of the keys are found, false otherwise.
  308. */
  309. public function has_all(array $keys) {
  310. foreach ($keys as $key) {
  311. if (!$this->has($key)) {
  312. return false;
  313. }
  314. }
  315. return true;
  316. }
  317. /**
  318. * Tries to acquire a lock with a given name.
  319. *
  320. * @see cache_is_lockable
  321. * @param string $key Name of the lock to acquire.
  322. * @param string $ownerid Information to identify owner of lock if acquired.
  323. * @return bool True if the lock was acquired, false if it was not.
  324. */
  325. public function acquire_lock($key, $ownerid) {
  326. return $this->redis->setnx($key, $ownerid);
  327. }
  328. /**
  329. * Checks a lock with a given name and owner information.
  330. *
  331. * @see cache_is_lockable
  332. * @param string $key Name of the lock to check.
  333. * @param string $ownerid Owner information to check existing lock against.
  334. * @return mixed True if the lock exists and the owner information matches, null if the lock does not
  335. * exist, and false otherwise.
  336. */
  337. public function check_lock_state($key, $ownerid) {
  338. $result = $this->redis->get($key);
  339. if ($result === $ownerid) {
  340. return true;
  341. }
  342. if ($result === false) {
  343. return null;
  344. }
  345. return false;
  346. }
  347. /**
  348. * Finds all of the keys being used by this cache store instance.
  349. *
  350. * @return array of all keys in the hash as a numbered array.
  351. */
  352. public function find_all() {
  353. return $this->redis->hKeys($this->hash);
  354. }
  355. /**
  356. * Finds all of the keys whose keys start with the given prefix.
  357. *
  358. * @param string $prefix
  359. *
  360. * @return array List of keys that match this prefix.
  361. */
  362. public function find_by_prefix($prefix) {
  363. $return = [];
  364. foreach ($this->find_all() as $key) {
  365. if (strpos($key, $prefix) === 0) {
  366. $return[] = $key;
  367. }
  368. }
  369. return $return;
  370. }
  371. /**
  372. * Releases a given lock if the owner information matches.
  373. *
  374. * @see cache_is_lockable
  375. * @param string $key Name of the lock to release.
  376. * @param string $ownerid Owner information to use.
  377. * @return bool True if the lock is released, false if it is not.
  378. */
  379. public function release_lock($key, $ownerid) {
  380. if ($this->check_lock_state($key, $ownerid)) {
  381. return ($this->redis->del($key) !== false);
  382. }
  383. return false;
  384. }
  385. /**
  386. * Creates a configuration array from given 'add instance' form data.
  387. *
  388. * @see cache_is_configurable
  389. * @param stdClass $data
  390. * @return array
  391. */
  392. public static function config_get_configuration_array($data) {
  393. return array('server' => $data->server, 'prefix' => $data->prefix);
  394. }
  395. /**
  396. * Sets form data from a configuration array.
  397. *
  398. * @see cache_is_configurable
  399. * @param moodleform $editform
  400. * @param array $config
  401. */
  402. public static function config_set_edit_form_data(moodleform $editform, array $config) {
  403. $data = array();
  404. $data['server'] = $config['server'];
  405. $data['prefix'] = !empty($config['prefix']) ? $config['prefix'] : '';
  406. $editform->set_data($data);
  407. }
  408. /**
  409. * Creates an instance of the store for testing.
  410. *
  411. * @param cache_definition $definition
  412. * @return mixed An instance of the store, or false if an instance cannot be created.
  413. */
  414. public static function initialise_test_instance(cache_definition $definition) {
  415. if (!self::are_requirements_met()) {
  416. return false;
  417. }
  418. $config = get_config('cachestore_redis');
  419. if (empty($config->test_server)) {
  420. return false;
  421. }
  422. $cache = new cachestore_redis('Redis test', ['server' => $config->test_server]);
  423. $cache->initialise($definition);
  424. return $cache;
  425. }
  426. /**
  427. * Return configuration to use when unit testing.
  428. *
  429. * @return array
  430. */
  431. public static function unit_test_configuration() {
  432. global $DB;
  433. if (!self::are_requirements_met() || !self::ready_to_be_used_for_testing()) {
  434. throw new moodle_exception('TEST_CACHESTORE_REDIS_TESTSERVERS not configured, unable to create test configuration');
  435. }
  436. return ['server' => TEST_CACHESTORE_REDIS_TESTSERVERS,
  437. 'prefix' => $DB->get_prefix(),
  438. ];
  439. }
  440. /**
  441. * Returns true if this cache store instance is both suitable for testing, and ready for testing.
  442. *
  443. * When TEST_CACHESTORE_REDIS_TESTSERVERS is set, then we are ready to be use d for testing.
  444. *
  445. * @return bool
  446. */
  447. public static function ready_to_be_used_for_testing() {
  448. return defined('TEST_CACHESTORE_REDIS_TESTSERVERS');
  449. }
  450. }