MDL-54592 cachestore_mongodb: MongoDB cache store use new driver.
[moodle.git] / cache / stores / mongodb / lib.php
CommitLineData
2e638e3d
SH
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 *
6fec1820 22 * @package cachestore_mongodb
2e638e3d
SH
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
2a944b5b
VDF
29require_once('MongoDB/functions.php');
30
2e638e3d
SH
31/**
32 * The MongoDB Cache store.
33 *
2a944b5b 34 * This cache store uses the MongoDB Native Driver and the MongoDB PHP Library.
2e638e3d 35 * For installation instructions have a look at the following two links:
2a944b5b
VDF
36 * - {@link http://php.net/manual/en/set.mongodb.php}
37 * - {@link https://docs.mongodb.com/ecosystem/drivers/php/}
2e638e3d
SH
38 *
39 * @copyright 2012 Sam Hemelryk
40 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41 */
2b274ad0 42class cachestore_mongodb extends cache_store implements cache_is_configurable {
2e638e3d
SH
43
44 /**
45 * The name of the store
46 * @var string
47 */
48 protected $name;
49
50 /**
51 * The server connection string. Comma separated values.
52 * @var string
53 */
54 protected $server = 'mongodb://127.0.0.1:27017';
55
56 /**
57 * The database connection options
58 * @var array
59 */
60 protected $options = array();
61
62 /**
63 * The name of the database to use.
64 * @var string
65 */
66 protected $databasename = 'mcache';
67
68 /**
69 * The Connection object
2a944b5b 70 * @var MongoDB/Client
2e638e3d 71 */
a037c943 72 protected $connection = false;
2e638e3d
SH
73
74 /**
75 * The Database Object
2a944b5b 76 * @var MongoDB/Database
2e638e3d
SH
77 */
78 protected $database;
79
80 /**
81 * The Collection object
2a944b5b 82 * @var MongoDB/Collection
2e638e3d
SH
83 */
84 protected $collection;
85
86 /**
87 * Determines if and what safe setting is to be used.
88 * @var bool|int
89 */
63b159d0 90 protected $usesafe = true;
2e638e3d
SH
91
92 /**
93 * If set to true then multiple identifiers will be requested and used.
94 * @var bool
95 */
96 protected $extendedmode = false;
97
98 /**
99 * The definition has which is used in the construction of the collection.
100 * @var string
101 */
102 protected $definitionhash = null;
103
104 /**
5dfa3031
SH
105 * Set to true once this store is ready to be initialised and used.
106 * @var bool
107 */
108 protected $isready = false;
109
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 *
2e638e3d
SH
116 * @param string $name
117 * @param array $configuration
118 */
119 public function __construct($name, array $configuration = array()) {
120 $this->name = $name;
121
122 if (array_key_exists('server', $configuration)) {
123 $this->server = $configuration['server'];
124 }
125
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 }
144
f4cec2ec 145 try {
2a944b5b 146 $this->connection = new MongoDB\Client($this->server, $this->options);
f4cec2ec 147 $this->isready = true;
2a944b5b
VDF
148 } catch (MongoDB\Driver\Exception\RuntimeException $e) {
149 // We only want to catch RuntimeException here.
f4cec2ec 150 }
2e638e3d
SH
151 }
152
153 /**
154 * Returns true if the requirements of this store have been met.
155 * @return bool
156 */
157 public static function are_requirements_met() {
2a944b5b 158 return version_compare(phpversion('mongodb'), '1.5', 'ge');
2e638e3d
SH
159 }
160
2e638e3d
SH
161 /**
162 * Returns the supported features.
163 * @param array $configuration
164 * @return int
165 */
166 public static function get_supported_features(array $configuration = array()) {
b2159f2d 167 $supports = self::SUPPORTS_DATA_GUARANTEE + self::DEREFERENCES_OBJECTS;
2e638e3d
SH
168 if (array_key_exists('extendedmode', $configuration) && $configuration['extendedmode']) {
169 $supports += self::SUPPORTS_MULTIPLE_IDENTIFIERS;
170 }
171 return $supports;
172 }
173
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()) {
90a3a620 180 return self::MODE_APPLICATION;
2e638e3d
SH
181 }
182
183 /**
184 * Initialises the store instance for use.
185 *
5dfa3031 186 * Once this has been done the cache is all set to be used.
2e638e3d
SH
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 }
2a944b5b 195 $this->database = $this->connection->selectDatabase($this->databasename);
c9ef35e1 196 $this->definitionhash = 'm'.$definition->generate_definition_hash();
2e638e3d 197 $this->collection = $this->database->selectCollection($this->definitionhash);
4907a8ed
SH
198
199 $options = array('name' => 'idx_key');
2a944b5b
VDF
200
201 $w = $this->usesafe ? 1 : 0;
202 $wc = new MongoDB\Driver\WriteConcern($w);
203
204 $options['writeConcern'] = $wc;
205
206 $this->collection->createIndex(array('key' => 1), $options);
2e638e3d
SH
207 }
208
209 /**
210 * Returns true if this store instance has been initialised.
211 * @return bool
212 */
213 public function is_initialised() {
2a944b5b 214 return ($this->database instanceof MongoDB\Database);
2e638e3d
SH
215 }
216
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 }
224
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 }
233
2e638e3d
SH
234 /**
235 * Returns true if this store is making use of multiple identifiers.
236 * @return bool
237 */
758dbdf8 238 public function supports_multiple_identifiers() {
2e638e3d
SH
239 return $this->extendedmode;
240 }
241
2e638e3d
SH
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 }
170f821b 252
2e638e3d
SH
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 }
260
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) {
32c981e6 284 $id = (string)$result['key'];
2e638e3d
SH
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 }
294
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);
4907a8ed 311 $options = array('upsert' => true);
2a944b5b
VDF
312
313 $w = $this->usesafe ? 1 : 0;
314 $wc = new MongoDB\Driver\WriteConcern($w);
315
316 $options['writeConcern'] = $wc;
317
2e638e3d 318 $this->delete($key);
2a944b5b
VDF
319 try {
320 $this->collection->insertOne($record, $options);
321 } catch (MongoDB\Exception\Exception $e) {
322 return false;
32c981e6 323 }
2a944b5b
VDF
324
325 return true;
2e638e3d
SH
326 }
327
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']);
32c981e6 340 if ($result === true) {
2e638e3d
SH
341 $count++;
342 }
343 }
32c981e6 344 return $count;
2e638e3d
SH
345 }
346
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 }
4907a8ed 361 $options = array('justOne' => false);
2a944b5b
VDF
362
363 $w = $this->usesafe ? 1 : 0;
364 $wc = new MongoDB\Driver\WriteConcern($w);
365
366 $options['writeConcern'] = $wc;
367
368 try {
369 $result = $this->collection->deleteOne($criteria, $options);
370 } catch (\MongoDB\Exception $e) {
371 return false;
372 }
373
374 if (empty($result->getDeletedCount())) {
375 return false;
2e638e3d 376 }
2a944b5b
VDF
377
378 return true;
2e638e3d
SH
379 }
380
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 }
396
397 /**
398 * Purges the cache deleting all items within it.
399 *
400 * @return boolean True on success. False otherwise.
401 */
402 public function purge() {
f4cec2ec
MS
403 if ($this->isready) {
404 $this->collection->drop();
405 $this->collection = $this->database->selectCollection($this->definitionhash);
406 }
407
408 return true;
2e638e3d
SH
409 }
410
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;
d837df0d 436 $return['usesafevalue'] = $return['usesafe'];
2e638e3d
SH
437 }
438 }
439 return $return;
440 }
441
81ede547
SH
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 }
d837df0d
SH
456 if (isset($config['extendedmode'])) {
457 $data['extendedmode'] = (bool)$config['extendedmode'];
81ede547
SH
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 }
d837df0d
SH
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'];
81ede547
SH
473 }
474 $editform->set_data($data);
475 }
476
2e638e3d
SH
477 /**
478 * Performs any necessary clean up when the store instance is being deleted.
479 */
59ca73ff 480 public function instance_deleted() {
a037c943
SH
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 {
bb4c3916 486 try {
2a944b5b
VDF
487 $connection = new MongoDB\Client($this->server, $this->options);
488 } catch (MongoDB\Driver\Exception\RuntimeException $e) {
489 // We only want to catch RuntimeException here.
bb4c3916
SH
490 // If the server cannot be connected to we cannot clean it.
491 return;
492 }
a037c943 493 }
2a944b5b 494 $database = $connection->selectDatabase($this->databasename);
a037c943
SH
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;
59ca73ff
MS
502 }
503
2e638e3d
SH
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 }
514
6fec1820 515 $config = get_config('cachestore_mongodb');
2e638e3d
SH
516 if (empty($config->testserver)) {
517 return false;
518 }
2e638e3d
SH
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 }
32c981e6 533 $configuration['usesafe'] = 1;
2e638e3d
SH
534 if (!empty($config->testextendedmode)) {
535 $configuration['extendedmode'] = (bool)$config->testextendedmode;
536 }
537
6fec1820 538 $store = new cachestore_mongodb('Test mongodb', $configuration);
c1db791e
SH
539 if (!$store->is_ready()) {
540 return false;
541 }
2e638e3d
SH
542 $store->initialise($definition);
543
544 return $store;
545 }
34c84c72 546
eefb680d
SH
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 */
a169739d 553 public static function unit_test_configuration() {
eefb680d 554 $configuration = array();
c9ef35e1 555 $configuration['usesafe'] = 1;
eefb680d 556
a169739d
RS
557 // If the configuration is not defined correctly, return only the configuration know about.
558 if (defined('TEST_CACHESTORE_MONGODB_TESTSERVER')) {
2a944b5b 559 $configuration['server'] = TEST_CACHESTORE_MONGODB_TESTSERVER;
c1db791e 560 }
eefb680d 561
a169739d 562 return $configuration;
eefb680d
SH
563 }
564
34c84c72
SH
565 /**
566 * Returns the name of this instance.
567 * @return string
568 */
569 public function my_name() {
570 return $this->name;
571 }
63b159d0
SH
572
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 }
a3f3ea26 584}