4405da4b4cfa8c971d49727cb2e29c9527fca6db
[moodle.git] / cache / stores / mongodb / lib.php
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/>.
17 /**
18  * The library file for the MongoDB store plugin.
19  *
20  * This file is part of the MongoDB store plugin, it contains the API for interacting with an instance of the store.
21  *
22  * @package    cachestore_mongodb
23  * @copyright  2012 Sam Hemelryk
24  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 defined('MOODLE_INTERNAL') || die();
29 /**
30  * The MongoDB Cache store.
31  *
32  * This cache store uses the MongoDB Native Driver.
33  * For installation instructions have a look at the following two links:
34  *  - {@link http://www.php.net/manual/en/mongo.installation.php}
35  *  - {@link http://www.mongodb.org/display/DOCS/PHP+Language+Center}
36  *
37  * @copyright  2012 Sam Hemelryk
38  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  */
40 class cachestore_mongodb extends cache_store implements cache_is_configurable {
42     /**
43      * The name of the store
44      * @var string
45      */
46     protected $name;
48     /**
49      * The server connection string. Comma separated values.
50      * @var string
51      */
52     protected $server = 'mongodb://127.0.0.1:27017';
54     /**
55      * The database connection options
56      * @var array
57      */
58     protected $options = array();
60     /**
61      * The name of the database to use.
62      * @var string
63      */
64     protected $databasename = 'mcache';
66     /**
67      * The Connection object
68      * @var Mongo
69      */
70     protected $connection = false;
72     /**
73      * The Database Object
74      * @var MongoDB
75      */
76     protected $database;
78     /**
79      * The Collection object
80      * @var MongoCollection
81      */
82     protected $collection;
84     /**
85      * Determines if and what safe setting is to be used.
86      * @var bool|int
87      */
88     protected $usesafe = false;
90     /**
91      * If set to true then multiple identifiers will be requested and used.
92      * @var bool
93      */
94     protected $extendedmode = false;
96     /**
97      * The definition has which is used in the construction of the collection.
98      * @var string
99      */
100     protected $definitionhash = null;
102     /**
103      * Set to true once this store is ready to be initialised and used.
104      * @var bool
105      */
106     protected $isready = false;
108     /**
109      * Set to true if the Mongo extension is < version 1.3.
110      * If this is the case we must use the legacy Mongo class instead of MongoClient.
111      * Mongo is backwards compatible, although obviously deprecated.
112      * @var bool
113      */
114     protected $legacymongo = false;
116     /**
117      * Constructs a new instance of the Mongo store.
118      *
119      * Noting that this function is not an initialisation. It is used to prepare the store for use.
120      * The store will be initialised when required and will be provided with a cache_definition at that time.
121      *
122      * @param string $name
123      * @param array $configuration
124      */
125     public function __construct($name, array $configuration = array()) {
126         $this->name = $name;
128         if (array_key_exists('server', $configuration)) {
129             $this->server = $configuration['server'];
130         }
132         if (array_key_exists('replicaset', $configuration)) {
133             $this->options['replicaSet'] = (string)$configuration['replicaset'];
134         }
135         if (array_key_exists('username', $configuration) && !empty($configuration['username'])) {
136             $this->options['username'] = (string)$configuration['username'];
137         }
138         if (array_key_exists('password', $configuration) && !empty($configuration['password'])) {
139             $this->options['password'] = (string)$configuration['password'];
140         }
141         if (array_key_exists('database', $configuration)) {
142             $this->databasename = (string)$configuration['database'];
143         }
144         if (array_key_exists('usesafe', $configuration)) {
145             $this->usesafe = $configuration['usesafe'];
146         }
147         if (array_key_exists('extendedmode', $configuration)) {
148             $this->extendedmode = $configuration['extendedmode'];
149         }
151         // Test if the MongoClient class exists, if not we need to switch to legacy classes.
152         $this->legacymongo = (!class_exists('MongoClient'));
154         // MongoClient from Mongo 1.3 onwards. Mongo for earlier versions.
155         $class = ($this->legacymongo) ? 'Mongo' : 'MongoClient';
156         try {
157             $this->connection = new $class($this->server, $this->options);
158             $this->isready = true;
159         } catch (MongoConnectionException $e) {
160             // We only want to catch MongoConnectionExceptions here.
161         }
162     }
164     /**
165      * Returns true if the requirements of this store have been met.
166      * @return bool
167      */
168     public static function are_requirements_met() {
169         return class_exists('MongoClient') || class_exists('Mongo');
170     }
172     /**
173      * Returns the supported features.
174      * @param array $configuration
175      * @return int
176      */
177     public static function get_supported_features(array $configuration = array()) {
178         $supports = self::SUPPORTS_DATA_GUARANTEE;
179         if (array_key_exists('extendedmode', $configuration) && $configuration['extendedmode']) {
180             $supports += self::SUPPORTS_MULTIPLE_IDENTIFIERS;
181         }
182         return $supports;
183     }
185     /**
186      * Returns an int describing the supported modes.
187      * @param array $configuration
188      * @return int
189      */
190     public static function get_supported_modes(array $configuration = array()) {
191         return self::MODE_APPLICATION;
192     }
194     /**
195      * Initialises the store instance for use.
196      *
197      * Once this has been done the cache is all set to be used.
198      *
199      * @param cache_definition $definition
200      * @throws coding_exception
201      */
202     public function initialise(cache_definition $definition) {
203         if ($this->is_initialised()) {
204             throw new coding_exception('This mongodb instance has already been initialised.');
205         }
206         $this->database = $this->connection->selectDB($this->databasename);
207         $this->definitionhash = 'm'.$definition->generate_definition_hash();
208         $this->collection = $this->database->selectCollection($this->definitionhash);
210         $options = array('name' => 'idx_key');
211         if ($this->legacymongo) {
212             $options['safe'] = $this->usesafe;
213         } else {
214             $options['w'] = $this->usesafe ? 1 : 0;
215         }
216         $this->collection->ensureIndex(array('key' => 1), $options);
217     }
219     /**
220      * Returns true if this store instance has been initialised.
221      * @return bool
222      */
223     public function is_initialised() {
224         return ($this->database instanceof MongoDB);
225     }
227     /**
228      * Returns true if this store instance is ready to use.
229      * @return bool
230      */
231     public function is_ready() {
232         return $this->isready;
233     }
235     /**
236      * Returns true if the given mode is supported by this store.
237      * @param int $mode
238      * @return bool
239      */
240     public static function is_supported_mode($mode) {
241         return ($mode == self::MODE_APPLICATION || $mode == self::MODE_SESSION);
242     }
244     /**
245      * Returns true if this store is making use of multiple identifiers.
246      * @return bool
247      */
248     public function supports_multiple_identifiers() {
249         return $this->extendedmode;
250     }
252     /**
253      * Retrieves an item from the cache store given its key.
254      *
255      * @param string $key The key to retrieve
256      * @return mixed The data that was associated with the key, or false if the key did not exist.
257      */
258     public function get($key) {
259         if (!is_array($key)) {
260             $key = array('key' => $key);
261         }
263         $result = $this->collection->findOne($key);
264         if ($result === null || !array_key_exists('data', $result)) {
265             return false;
266         }
267         $data = @unserialize($result['data']);
268         return $data;
269     }
271     /**
272      * Retrieves several items from the cache store in a single transaction.
273      *
274      * If not all of the items are available in the cache then the data value for those that are missing will be set to false.
275      *
276      * @param array $keys The array of keys to retrieve
277      * @return array An array of items from the cache.
278      */
279     public function get_many($keys) {
280         if ($this->extendedmode) {
281             $query = $this->get_many_extendedmode_query($keys);
282             $keyarray = array();
283             foreach ($keys as $key) {
284                 $keyarray[] = $key['key'];
285             }
286             $keys = $keyarray;
287             $query = array('key' => array('$in' => $keys));
288         } else {
289             $query = array('key' => array('$in' => $keys));
290         }
291         $cursor = $this->collection->find($query);
292         $results = array();
293         foreach ($cursor as $result) {
294             $id = (string)$result['key'];
295             $results[$id] = unserialize($result['data']);
296         }
297         foreach ($keys as $key) {
298             if (!array_key_exists($key, $results)) {
299                 $results[$key] = false;
300             }
301         }
302         return $results;
303     }
305     /**
306      * Sets an item in the cache given its key and data value.
307      *
308      * @param string $key The key to use.
309      * @param mixed $data The data to set.
310      * @return bool True if the operation was a success false otherwise.
311      */
312     public function set($key, $data) {
313         if (!is_array($key)) {
314             $record = array(
315                 'key' => $key
316             );
317         } else {
318             $record = $key;
319         }
320         $record['data'] = serialize($data);
321         $options = array('upsert' => true);
322         if ($this->legacymongo) {
323             $options['safe'] = $this->usesafe;
324         } else {
325             $options['w'] = $this->usesafe ? 1 : 0;
326         }
327         $this->delete($key);
328         $result = $this->collection->insert($record, $options);
329         if ($result === true) {
330             // Safe mode is off.
331             return true;
332         } else if (is_array($result)) {
333             if (empty($result['ok']) || isset($result['err'])) {
334                 return false;
335             }
336             return true;
337         }
338         // Who knows?
339         return false;
340     }
342     /**
343      * Sets many items in the cache in a single transaction.
344      *
345      * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
346      *      keys, 'key' and 'value'.
347      * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
348      *      sent ... if they care that is.
349      */
350     public function set_many(array $keyvaluearray) {
351         $count = 0;
352         foreach ($keyvaluearray as $pair) {
353             $result = $this->set($pair['key'], $pair['value']);
354             if ($result === true) {
355                  $count++;
356             }
357         }
358         return $count;
359     }
361     /**
362      * Deletes an item from the cache store.
363      *
364      * @param string $key The key to delete.
365      * @return bool Returns true if the operation was a success, false otherwise.
366      */
367     public function delete($key) {
368         if (!is_array($key)) {
369             $criteria = array(
370                 'key' => $key
371             );
372         } else {
373             $criteria = $key;
374         }
375         $options = array('justOne' => false);
376         if ($this->legacymongo) {
377             $options['safe'] = $this->usesafe;
378         } else {
379             $options['w'] = $this->usesafe ? 1 : 0;
380         }
381         $result = $this->collection->remove($criteria, $options);
383         if ($result === true) {
384             // Safe mode.
385             return true;
386         } else if (is_array($result)) {
387             if (empty($result['ok']) || isset($result['err'])) {
388                 return false;
389             } else if (empty($result['n'])) {
390                 // Nothing was removed.
391                 return false;
392             }
393             return true;
394         }
395         // Who knows?
396         return false;
397     }
399     /**
400      * Deletes several keys from the cache in a single action.
401      *
402      * @param array $keys The keys to delete
403      * @return int The number of items successfully deleted.
404      */
405     public function delete_many(array $keys) {
406         $count = 0;
407         foreach ($keys as $key) {
408             if ($this->delete($key)) {
409                 $count++;
410             }
411         }
412         return $count;
413     }
415     /**
416      * Purges the cache deleting all items within it.
417      *
418      * @return boolean True on success. False otherwise.
419      */
420     public function purge() {
421         if ($this->isready) {
422             $this->collection->drop();
423             $this->collection = $this->database->selectCollection($this->definitionhash);
424         }
426         return true;
427     }
429     /**
430      * Takes the object from the add instance store and creates a configuration array that can be used to initialise an instance.
431      *
432      * @param stdClass $data
433      * @return array
434      */
435     public static function config_get_configuration_array($data) {
436         $return = array(
437             'server' => $data->server,
438             'database' => $data->database,
439             'extendedmode' => (!empty($data->extendedmode))
440         );
441         if (!empty($data->username)) {
442             $return['username'] = $data->username;
443         }
444         if (!empty($data->password)) {
445             $return['password'] = $data->password;
446         }
447         if (!empty($data->replicaset)) {
448             $return['replicaset'] = $data->replicaset;
449         }
450         if (!empty($data->usesafe)) {
451             $return['usesafe'] = true;
452             if (!empty($data->usesafevalue)) {
453                 $return['usesafe'] = (int)$data->usesafevalue;
454                 $return['usesafevalue'] = $return['usesafe'];
455             }
456         }
457         return $return;
458     }
460     /**
461      * Allows the cache store to set its data against the edit form before it is shown to the user.
462      *
463      * @param moodleform $editform
464      * @param array $config
465      */
466     public static function config_set_edit_form_data(moodleform $editform, array $config) {
467         $data = array();
468         if (!empty($config['server'])) {
469             $data['server'] = $config['server'];
470         }
471         if (!empty($config['database'])) {
472             $data['database'] = $config['database'];
473         }
474         if (isset($config['extendedmode'])) {
475             $data['extendedmode'] = (bool)$config['extendedmode'];
476         }
477         if (!empty($config['username'])) {
478             $data['username'] = $config['username'];
479         }
480         if (!empty($config['password'])) {
481             $data['password'] = $config['password'];
482         }
483         if (!empty($config['replicaset'])) {
484             $data['replicaset'] = $config['replicaset'];
485         }
486         if (isset($config['usesafevalue'])) {
487             $data['usesafe'] = true;
488             $data['usesafevalue'] = (int)$data['usesafe'];
489         } else if (isset($config['usesafe'])) {
490             $data['usesafe'] = (bool)$config['usesafe'];
491         }
492         $editform->set_data($data);
493     }
495     /**
496      * Performs any necessary clean up when the store instance is being deleted.
497      */
498     public function instance_deleted() {
499         // We can't use purge here that acts upon a collection.
500         // Instead we must drop the named database.
501         if ($this->connection) {
502             $connection = $this->connection;
503         } else {
504             try {
505                 // MongoClient from Mongo 1.3 onwards. Mongo for earlier versions.
506                 $class = ($this->legacymongo) ? 'Mongo' : 'MongoClient';
507                 $connection = new $class($this->server, $this->options);
508             } catch (MongoConnectionException $e) {
509                 // We only want to catch MongoConnectionExceptions here.
510                 // If the server cannot be connected to we cannot clean it.
511                 return;
512             }
513         }
514         $database = $connection->selectDB($this->databasename);
515         $database->drop();
516         $connection = null;
517         $database = null;
518         // Explicitly unset things to cause a close.
519         $this->collection = null;
520         $this->database = null;
521         $this->connection = null;
522     }
524     /**
525      * Generates an instance of the cache store that can be used for testing.
526      *
527      * @param cache_definition $definition
528      * @return false
529      */
530     public static function initialise_test_instance(cache_definition $definition) {
531         if (!self::are_requirements_met()) {
532             return false;
533         }
535         $config = get_config('cachestore_mongodb');
536         if (empty($config->testserver)) {
537             return false;
538         }
539         $configuration = array();
540         $configuration['server'] = $config->testserver;
541         if (!empty($config->testreplicaset)) {
542             $configuration['replicaset'] = $config->testreplicaset;
543         }
544         if (!empty($config->testusername)) {
545             $configuration['username'] = $config->testusername;
546         }
547         if (!empty($config->testpassword)) {
548             $configuration['password'] = $config->testpassword;
549         }
550         if (!empty($config->testdatabase)) {
551             $configuration['database'] = $config->testdatabase;
552         }
553         $configuration['usesafe'] = 1;
554         if (!empty($config->testextendedmode)) {
555             $configuration['extendedmode'] = (bool)$config->testextendedmode;
556         }
558         $store = new cachestore_mongodb('Test mongodb', $configuration);
559         $store->initialise($definition);
561         return $store;
562     }
565     /**
566      * Generates an instance of the cache store that can be used for testing.
567      *
568      * @param cache_definition $definition
569      * @return false
570      */
571     public static function initialise_unit_test_instance(cache_definition $definition) {
572         if (!self::are_requirements_met()) {
573             return false;
574         }
575         if (!defined('TEST_CACHESTORE_MONGODB_TESTSERVER')) {
576             return false;
577         }
579         $configuration = array();
580         $configuration['servers'] = explode("\n", TEST_CACHESTORE_MONGODB_TESTSERVER);
581         $configuration['usesafe'] = 1;
583         $store = new cachestore_mongodb('Test mongodb', $configuration);
584         $store->initialise($definition);
586         return $store;
587     }
589     /**
590      * Returns the name of this instance.
591      * @return string
592      */
593     public function my_name() {
594         return $this->name;
595     }