MDL-54592 cachestore_mongodb: MongoDB cache store use new driver.
[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 require_once('MongoDB/functions.php');
31 /**
32  * The MongoDB Cache store.
33  *
34  * This cache store uses the MongoDB Native Driver and the MongoDB PHP Library.
35  * For installation instructions have a look at the following two links:
36  *  - {@link http://php.net/manual/en/set.mongodb.php}
37  *  - {@link https://docs.mongodb.com/ecosystem/drivers/php/}
38  *
39  * @copyright  2012 Sam Hemelryk
40  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41  */
42 class cachestore_mongodb extends cache_store implements cache_is_configurable {
44     /**
45      * The name of the store
46      * @var string
47      */
48     protected $name;
50     /**
51      * The server connection string. Comma separated values.
52      * @var string
53      */
54     protected $server = 'mongodb://127.0.0.1:27017';
56     /**
57      * The database connection options
58      * @var array
59      */
60     protected $options = array();
62     /**
63      * The name of the database to use.
64      * @var string
65      */
66     protected $databasename = 'mcache';
68     /**
69      * The Connection object
70      * @var MongoDB/Client
71      */
72     protected $connection = false;
74     /**
75      * The Database Object
76      * @var MongoDB/Database
77      */
78     protected $database;
80     /**
81      * The Collection object
82      * @var MongoDB/Collection
83      */
84     protected $collection;
86     /**
87      * Determines if and what safe setting is to be used.
88      * @var bool|int
89      */
90     protected $usesafe = true;
92     /**
93      * If set to true then multiple identifiers will be requested and used.
94      * @var bool
95      */
96     protected $extendedmode = false;
98     /**
99      * The definition has which is used in the construction of the collection.
100      * @var string
101      */
102     protected $definitionhash = null;
104     /**
105      * Set to true once this store is ready to be initialised and used.
106      * @var bool
107      */
108     protected $isready = false;
110     /**
111      * Constructs a new instance of the Mongo store.
112      *
113      * Noting that this function is not an initialisation. It is used to prepare the store for use.
114      * The store will be initialised when required and will be provided with a cache_definition at that time.
115      *
116      * @param string $name
117      * @param array $configuration
118      */
119     public function __construct($name, array $configuration = array()) {
120         $this->name = $name;
122         if (array_key_exists('server', $configuration)) {
123             $this->server = $configuration['server'];
124         }
126         if (array_key_exists('replicaset', $configuration)) {
127             $this->options['replicaSet'] = (string)$configuration['replicaset'];
128         }
129         if (array_key_exists('username', $configuration) && !empty($configuration['username'])) {
130             $this->options['username'] = (string)$configuration['username'];
131         }
132         if (array_key_exists('password', $configuration) && !empty($configuration['password'])) {
133             $this->options['password'] = (string)$configuration['password'];
134         }
135         if (array_key_exists('database', $configuration)) {
136             $this->databasename = (string)$configuration['database'];
137         }
138         if (array_key_exists('usesafe', $configuration)) {
139             $this->usesafe = $configuration['usesafe'];
140         }
141         if (array_key_exists('extendedmode', $configuration)) {
142             $this->extendedmode = $configuration['extendedmode'];
143         }
145         try {
146             $this->connection = new MongoDB\Client($this->server, $this->options);
147             $this->isready = true;
148         } catch (MongoDB\Driver\Exception\RuntimeException $e) {
149             // We only want to catch RuntimeException here.
150         }
151     }
153     /**
154      * Returns true if the requirements of this store have been met.
155      * @return bool
156      */
157     public static function are_requirements_met() {
158         return version_compare(phpversion('mongodb'), '1.5', 'ge');
159     }
161     /**
162      * Returns the supported features.
163      * @param array $configuration
164      * @return int
165      */
166     public static function get_supported_features(array $configuration = array()) {
167         $supports = self::SUPPORTS_DATA_GUARANTEE + self::DEREFERENCES_OBJECTS;
168         if (array_key_exists('extendedmode', $configuration) && $configuration['extendedmode']) {
169             $supports += self::SUPPORTS_MULTIPLE_IDENTIFIERS;
170         }
171         return $supports;
172     }
174     /**
175      * Returns an int describing the supported modes.
176      * @param array $configuration
177      * @return int
178      */
179     public static function get_supported_modes(array $configuration = array()) {
180         return self::MODE_APPLICATION;
181     }
183     /**
184      * Initialises the store instance for use.
185      *
186      * Once this has been done the cache is all set to be used.
187      *
188      * @param cache_definition $definition
189      * @throws coding_exception
190      */
191     public function initialise(cache_definition $definition) {
192         if ($this->is_initialised()) {
193             throw new coding_exception('This mongodb instance has already been initialised.');
194         }
195         $this->database = $this->connection->selectDatabase($this->databasename);
196         $this->definitionhash = 'm'.$definition->generate_definition_hash();
197         $this->collection = $this->database->selectCollection($this->definitionhash);
199         $options = array('name' => 'idx_key');
201         $w = $this->usesafe ? 1 : 0;
202         $wc = new MongoDB\Driver\WriteConcern($w);
204         $options['writeConcern'] = $wc;
206         $this->collection->createIndex(array('key' => 1), $options);
207     }
209     /**
210      * Returns true if this store instance has been initialised.
211      * @return bool
212      */
213     public function is_initialised() {
214         return ($this->database instanceof MongoDB\Database);
215     }
217     /**
218      * Returns true if this store instance is ready to use.
219      * @return bool
220      */
221     public function is_ready() {
222         return $this->isready;
223     }
225     /**
226      * Returns true if the given mode is supported by this store.
227      * @param int $mode
228      * @return bool
229      */
230     public static function is_supported_mode($mode) {
231         return ($mode == self::MODE_APPLICATION || $mode == self::MODE_SESSION);
232     }
234     /**
235      * Returns true if this store is making use of multiple identifiers.
236      * @return bool
237      */
238     public function supports_multiple_identifiers() {
239         return $this->extendedmode;
240     }
242     /**
243      * Retrieves an item from the cache store given its key.
244      *
245      * @param string $key The key to retrieve
246      * @return mixed The data that was associated with the key, or false if the key did not exist.
247      */
248     public function get($key) {
249         if (!is_array($key)) {
250             $key = array('key' => $key);
251         }
253         $result = $this->collection->findOne($key);
254         if ($result === null || !array_key_exists('data', $result)) {
255             return false;
256         }
257         $data = @unserialize($result['data']);
258         return $data;
259     }
261     /**
262      * Retrieves several items from the cache store in a single transaction.
263      *
264      * 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.
265      *
266      * @param array $keys The array of keys to retrieve
267      * @return array An array of items from the cache.
268      */
269     public function get_many($keys) {
270         if ($this->extendedmode) {
271             $query = $this->get_many_extendedmode_query($keys);
272             $keyarray = array();
273             foreach ($keys as $key) {
274                 $keyarray[] = $key['key'];
275             }
276             $keys = $keyarray;
277             $query = array('key' => array('$in' => $keys));
278         } else {
279             $query = array('key' => array('$in' => $keys));
280         }
281         $cursor = $this->collection->find($query);
282         $results = array();
283         foreach ($cursor as $result) {
284             $id = (string)$result['key'];
285             $results[$id] = unserialize($result['data']);
286         }
287         foreach ($keys as $key) {
288             if (!array_key_exists($key, $results)) {
289                 $results[$key] = false;
290             }
291         }
292         return $results;
293     }
295     /**
296      * Sets an item in the cache given its key and data value.
297      *
298      * @param string $key The key to use.
299      * @param mixed $data The data to set.
300      * @return bool True if the operation was a success false otherwise.
301      */
302     public function set($key, $data) {
303         if (!is_array($key)) {
304             $record = array(
305                 'key' => $key
306             );
307         } else {
308             $record = $key;
309         }
310         $record['data'] = serialize($data);
311         $options = array('upsert' => true);
313         $w = $this->usesafe ? 1 : 0;
314         $wc = new MongoDB\Driver\WriteConcern($w);
316         $options['writeConcern'] = $wc;
318         $this->delete($key);
319         try {
320             $this->collection->insertOne($record, $options);
321         } catch (MongoDB\Exception\Exception $e) {
322             return false;
323         }
325         return true;
326     }
328     /**
329      * Sets many items in the cache in a single transaction.
330      *
331      * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
332      *      keys, 'key' and 'value'.
333      * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
334      *      sent ... if they care that is.
335      */
336     public function set_many(array $keyvaluearray) {
337         $count = 0;
338         foreach ($keyvaluearray as $pair) {
339             $result = $this->set($pair['key'], $pair['value']);
340             if ($result === true) {
341                  $count++;
342             }
343         }
344         return $count;
345     }
347     /**
348      * Deletes an item from the cache store.
349      *
350      * @param string $key The key to delete.
351      * @return bool Returns true if the operation was a success, false otherwise.
352      */
353     public function delete($key) {
354         if (!is_array($key)) {
355             $criteria = array(
356                 'key' => $key
357             );
358         } else {
359             $criteria = $key;
360         }
361         $options = array('justOne' => false);
363         $w = $this->usesafe ? 1 : 0;
364         $wc = new MongoDB\Driver\WriteConcern($w);
366         $options['writeConcern'] = $wc;
368         try {
369             $result = $this->collection->deleteOne($criteria, $options);
370         } catch (\MongoDB\Exception $e) {
371             return false;
372         }
374         if (empty($result->getDeletedCount())) {
375             return false;
376         }
378         return true;
379     }
381     /**
382      * Deletes several keys from the cache in a single action.
383      *
384      * @param array $keys The keys to delete
385      * @return int The number of items successfully deleted.
386      */
387     public function delete_many(array $keys) {
388         $count = 0;
389         foreach ($keys as $key) {
390             if ($this->delete($key)) {
391                 $count++;
392             }
393         }
394         return $count;
395     }
397     /**
398      * Purges the cache deleting all items within it.
399      *
400      * @return boolean True on success. False otherwise.
401      */
402     public function purge() {
403         if ($this->isready) {
404             $this->collection->drop();
405             $this->collection = $this->database->selectCollection($this->definitionhash);
406         }
408         return true;
409     }
411     /**
412      * Takes the object from the add instance store and creates a configuration array that can be used to initialise an instance.
413      *
414      * @param stdClass $data
415      * @return array
416      */
417     public static function config_get_configuration_array($data) {
418         $return = array(
419             'server' => $data->server,
420             'database' => $data->database,
421             'extendedmode' => (!empty($data->extendedmode))
422         );
423         if (!empty($data->username)) {
424             $return['username'] = $data->username;
425         }
426         if (!empty($data->password)) {
427             $return['password'] = $data->password;
428         }
429         if (!empty($data->replicaset)) {
430             $return['replicaset'] = $data->replicaset;
431         }
432         if (!empty($data->usesafe)) {
433             $return['usesafe'] = true;
434             if (!empty($data->usesafevalue)) {
435                 $return['usesafe'] = (int)$data->usesafevalue;
436                 $return['usesafevalue'] = $return['usesafe'];
437             }
438         }
439         return $return;
440     }
442     /**
443      * Allows the cache store to set its data against the edit form before it is shown to the user.
444      *
445      * @param moodleform $editform
446      * @param array $config
447      */
448     public static function config_set_edit_form_data(moodleform $editform, array $config) {
449         $data = array();
450         if (!empty($config['server'])) {
451             $data['server'] = $config['server'];
452         }
453         if (!empty($config['database'])) {
454             $data['database'] = $config['database'];
455         }
456         if (isset($config['extendedmode'])) {
457             $data['extendedmode'] = (bool)$config['extendedmode'];
458         }
459         if (!empty($config['username'])) {
460             $data['username'] = $config['username'];
461         }
462         if (!empty($config['password'])) {
463             $data['password'] = $config['password'];
464         }
465         if (!empty($config['replicaset'])) {
466             $data['replicaset'] = $config['replicaset'];
467         }
468         if (isset($config['usesafevalue'])) {
469             $data['usesafe'] = true;
470             $data['usesafevalue'] = (int)$data['usesafe'];
471         } else if (isset($config['usesafe'])) {
472             $data['usesafe'] = (bool)$config['usesafe'];
473         }
474         $editform->set_data($data);
475     }
477     /**
478      * Performs any necessary clean up when the store instance is being deleted.
479      */
480     public function instance_deleted() {
481         // We can't use purge here that acts upon a collection.
482         // Instead we must drop the named database.
483         if ($this->connection) {
484             $connection = $this->connection;
485         } else {
486             try {
487                 $connection = new MongoDB\Client($this->server, $this->options);
488             } catch (MongoDB\Driver\Exception\RuntimeException $e) {
489                 // We only want to catch RuntimeException here.
490                 // If the server cannot be connected to we cannot clean it.
491                 return;
492             }
493         }
494         $database = $connection->selectDatabase($this->databasename);
495         $database->drop();
496         $connection = null;
497         $database = null;
498         // Explicitly unset things to cause a close.
499         $this->collection = null;
500         $this->database = null;
501         $this->connection = null;
502     }
504     /**
505      * Generates an instance of the cache store that can be used for testing.
506      *
507      * @param cache_definition $definition
508      * @return false
509      */
510     public static function initialise_test_instance(cache_definition $definition) {
511         if (!self::are_requirements_met()) {
512             return false;
513         }
515         $config = get_config('cachestore_mongodb');
516         if (empty($config->testserver)) {
517             return false;
518         }
519         $configuration = array();
520         $configuration['server'] = $config->testserver;
521         if (!empty($config->testreplicaset)) {
522             $configuration['replicaset'] = $config->testreplicaset;
523         }
524         if (!empty($config->testusername)) {
525             $configuration['username'] = $config->testusername;
526         }
527         if (!empty($config->testpassword)) {
528             $configuration['password'] = $config->testpassword;
529         }
530         if (!empty($config->testdatabase)) {
531             $configuration['database'] = $config->testdatabase;
532         }
533         $configuration['usesafe'] = 1;
534         if (!empty($config->testextendedmode)) {
535             $configuration['extendedmode'] = (bool)$config->testextendedmode;
536         }
538         $store = new cachestore_mongodb('Test mongodb', $configuration);
539         if (!$store->is_ready()) {
540             return false;
541         }
542         $store->initialise($definition);
544         return $store;
545     }
547     /**
548      * Generates an instance of the cache store that can be used for testing.
549      *
550      * @param cache_definition $definition
551      * @return false
552      */
553     public static function unit_test_configuration() {
554         $configuration = array();
555         $configuration['usesafe'] = 1;
557         // If the configuration is not defined correctly, return only the configuration know about.
558         if (defined('TEST_CACHESTORE_MONGODB_TESTSERVER')) {
559             $configuration['server'] = TEST_CACHESTORE_MONGODB_TESTSERVER;
560         }
562         return $configuration;
563     }
565     /**
566      * Returns the name of this instance.
567      * @return string
568      */
569     public function my_name() {
570         return $this->name;
571     }
573     /**
574      * Returns true if this cache store instance is both suitable for testing, and ready for testing.
575      *
576      * Cache stores that support being used as the default store for unit and acceptance testing should
577      * override this function and return true if there requirements have been met.
578      *
579      * @return bool
580      */
581     public static function ready_to_be_used_for_testing() {
582         return defined('TEST_CACHESTORE_MONGODB_TESTSERVER');
583     }