source: moodle/trunk/fuentes/cache/stores/mongodb/lib.php @ 1331

Last change on this file since 1331 was 1331, checked in by jrpelegrina, 3 years ago

Updated to moodle 3.0.3

File size: 19.2 KB
Line 
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/**
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 */
26
27defined('MOODLE_INTERNAL') || die();
28
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 */
40class cachestore_mongodb extends cache_store implements cache_is_configurable {
41
42    /**
43     * The name of the store
44     * @var string
45     */
46    protected $name;
47
48    /**
49     * The server connection string. Comma separated values.
50     * @var string
51     */
52    protected $server = 'mongodb://127.0.0.1:27017';
53
54    /**
55     * The database connection options
56     * @var array
57     */
58    protected $options = array();
59
60    /**
61     * The name of the database to use.
62     * @var string
63     */
64    protected $databasename = 'mcache';
65
66    /**
67     * The Connection object
68     * @var Mongo
69     */
70    protected $connection = false;
71
72    /**
73     * The Database Object
74     * @var MongoDB
75     */
76    protected $database;
77
78    /**
79     * The Collection object
80     * @var MongoCollection
81     */
82    protected $collection;
83
84    /**
85     * Determines if and what safe setting is to be used.
86     * @var bool|int
87     */
88    protected $usesafe = true;
89
90    /**
91     * If set to true then multiple identifiers will be requested and used.
92     * @var bool
93     */
94    protected $extendedmode = false;
95
96    /**
97     * The definition has which is used in the construction of the collection.
98     * @var string
99     */
100    protected $definitionhash = null;
101
102    /**
103     * Set to true once this store is ready to be initialised and used.
104     * @var bool
105     */
106    protected $isready = false;
107
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;
115
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;
127
128        if (array_key_exists('server', $configuration)) {
129            $this->server = $configuration['server'];
130        }
131
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        }
150
151        // Test if the MongoClient class exists, if not we need to switch to legacy classes.
152        $this->legacymongo = (!class_exists('MongoClient'));
153
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    }
163
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    }
171
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    }
184
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    }
193
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);
209
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    }
218
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    }
226
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    }
234
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    }
243
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    }
251
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        }
262
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    }
270
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    }
304
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    }
341
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    }
360
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);
382
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    }
398
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    }
414
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        }
425
426        return true;
427    }
428
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    }
459
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    }
494
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    }
523
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        }
534
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        }
557
558        $store = new cachestore_mongodb('Test mongodb', $configuration);
559        if (!$store->is_ready()) {
560            return false;
561        }
562        $store->initialise($definition);
563
564        return $store;
565    }
566
567
568    /**
569     * Generates an instance of the cache store that can be used for testing.
570     *
571     * @param cache_definition $definition
572     * @return false
573     */
574    public static function initialise_unit_test_instance(cache_definition $definition) {
575        if (!self::are_requirements_met()) {
576            return false;
577        }
578        if (!defined('TEST_CACHESTORE_MONGODB_TESTSERVER')) {
579            return false;
580        }
581
582        $configuration = array();
583        $configuration['servers'] = explode("\n", TEST_CACHESTORE_MONGODB_TESTSERVER);
584        $configuration['usesafe'] = 1;
585
586        $store = new cachestore_mongodb('Test mongodb', $configuration);
587        if (!$store->is_ready()) {
588            return false;
589        }
590        $store->initialise($definition);
591
592        return $store;
593    }
594
595    /**
596     * Returns the name of this instance.
597     * @return string
598     */
599    public function my_name() {
600        return $this->name;
601    }
602
603    /**
604     * Returns true if this cache store instance is both suitable for testing, and ready for testing.
605     *
606     * Cache stores that support being used as the default store for unit and acceptance testing should
607     * override this function and return true if there requirements have been met.
608     *
609     * @return bool
610     */
611    public static function ready_to_be_used_for_testing() {
612        return defined('TEST_CACHESTORE_MONGODB_TESTSERVER');
613    }
614}
Note: See TracBrowser for help on using the repository browser.