MDL-40903 cache: fixed up event invalidation
[moodle.git] / cache / classes / helper.php
CommitLineData
8139ad13
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 * Cache helper class
19 *
20 * This file is part of Moodle's cache API, affectionately called MUC.
21 * It contains the components that are requried in order to use caching.
22 *
23 * @package core
24 * @category cache
25 * @copyright 2012 Sam Hemelryk
26 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 */
28
29defined('MOODLE_INTERNAL') || die();
30
31/**
32 * The cache helper class.
33 *
34 * The cache helper class provides common functionality to the cache API and is useful to developers within to interact with
35 * the cache API in a general way.
36 *
37 * @package core
38 * @category cache
39 * @copyright 2012 Sam Hemelryk
40 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41 */
42class cache_helper {
43
44 /**
45 * Statistics gathered by the cache API during its operation will be used here.
46 * @static
47 * @var array
48 */
49 protected static $stats = array();
50
51 /**
52 * The instance of the cache helper.
53 * @var cache_helper
54 */
55 protected static $instance;
56
7634410b
SH
57 /**
58 * The site identifier used by the cache.
59 * Set the first time get_site_identifier is called.
60 * @var string
61 */
62 protected static $siteidentifier = null;
63
8139ad13
SH
64 /**
65 * Returns true if the cache API can be initialised before Moodle has finished initialising itself.
66 *
67 * This check is essential when trying to cache the likes of configuration information. It checks to make sure that the cache
68 * configuration file has been created which allows use to set up caching when ever is required.
69 *
70 * @return bool
71 */
72 public static function ready_for_early_init() {
73 return cache_config::config_file_exists();
74 }
75
76 /**
77 * Returns an instance of the cache_helper.
78 *
79 * This is designed for internal use only and acts as a static store.
80 * @staticvar null $instance
81 * @return cache_helper
82 */
83 protected static function instance() {
84 if (is_null(self::$instance)) {
85 self::$instance = new cache_helper();
86 }
87 return self::$instance;
88 }
89
90 /**
91 * Constructs an instance of the cache_helper class. Again for internal use only.
92 */
93 protected function __construct() {
94 // Nothing to do here, just making sure you can't get an instance of this.
95 }
96
97 /**
98 * Used as a data store for initialised definitions.
99 * @var array
100 */
101 protected $definitions = array();
102
103 /**
104 * Used as a data store for initialised cache stores
105 * We use this because we want to avoid establishing multiple instances of a single store.
106 * @var array
107 */
108 protected $stores = array();
109
110 /**
111 * Returns the class for use as a cache loader for the given mode.
112 *
113 * @param int $mode One of cache_store::MODE_
114 * @return string
115 * @throws coding_exception
116 */
117 public static function get_class_for_mode($mode) {
118 switch ($mode) {
119 case cache_store::MODE_APPLICATION :
120 return 'cache_application';
121 case cache_store::MODE_REQUEST :
122 return 'cache_request';
123 case cache_store::MODE_SESSION :
124 return 'cache_session';
125 }
126 throw new coding_exception('Unknown cache mode passed. Must be one of cache_store::MODE_*');
127 }
128
129 /**
130 * Returns the cache stores to be used with the given definition.
131 * @param cache_definition $definition
132 * @return array
133 */
134 public static function get_cache_stores(cache_definition $definition) {
135 $instance = cache_config::instance();
136 $stores = $instance->get_stores_for_definition($definition);
6fec1820 137 $stores = self::initialise_cachestore_instances($stores, $definition);
8139ad13
SH
138 return $stores;
139 }
140
141 /**
142 * Internal function for initialising an array of stores against a given cache definition.
143 *
144 * @param array $stores
145 * @param cache_definition $definition
146 * @return array
147 */
6fec1820 148 protected static function initialise_cachestore_instances(array $stores, cache_definition $definition) {
8139ad13
SH
149 $return = array();
150 $factory = cache_factory::instance();
151 foreach ($stores as $name => $details) {
152 $store = $factory->create_store_from_config($name, $details, $definition);
153 if ($store !== false) {
154 $return[] = $store;
155 }
156 }
157 return $return;
158 }
159
160 /**
34c84c72
SH
161 * Returns a cache_lock instance suitable for use with the store.
162 *
163 * @param cache_store $store
164 * @return cache_lock_interface
8139ad13 165 */
34c84c72 166 public static function get_cachelock_for_store(cache_store $store) {
8139ad13 167 $instance = cache_config::instance();
34c84c72
SH
168 $lockconf = $instance->get_lock_for_store($store->my_name());
169 $factory = cache_factory::instance();
170 return $factory->create_lock_instance($lockconf);
8139ad13
SH
171 }
172
173 /**
174 * Returns an array of plugins without using core methods.
175 *
176 * This function explicitly does NOT use core functions as it will in some circumstances be called before Moodle has
177 * finished initialising. This happens when loading configuration for instance.
178 *
179 * @return string
180 */
181 public static function early_get_cache_plugins() {
182 global $CFG;
183 $result = array();
184 $ignored = array('CVS', '_vti_cnf', 'simpletest', 'db', 'yui', 'tests');
185 $fulldir = $CFG->dirroot.'/cache/stores';
186 $items = new DirectoryIterator($fulldir);
187 foreach ($items as $item) {
188 if ($item->isDot() or !$item->isDir()) {
189 continue;
190 }
191 $pluginname = $item->getFilename();
192 if (in_array($pluginname, $ignored)) {
193 continue;
194 }
5f850735 195 if (!is_valid_plugin_name($pluginname)) {
170f821b 196 // Better ignore plugins with problematic names here.
8139ad13
SH
197 continue;
198 }
199 $result[$pluginname] = $fulldir.'/'.$pluginname;
200 unset($item);
201 }
202 unset($items);
203 return $result;
204 }
205
206 /**
207 * Invalidates a given set of keys from a given definition.
208 *
209 * @todo Invalidating by definition should also add to the event cache so that sessions can be invalidated (when required).
210 *
211 * @param string $component
212 * @param string $area
213 * @param array $identifiers
214 * @param array $keys
215 * @return boolean
216 */
217 public static function invalidate_by_definition($component, $area, array $identifiers = array(), $keys = array()) {
218 $cache = cache::make($component, $area, $identifiers);
219 if (is_array($keys)) {
220 $cache->delete_many($keys);
221 } else if (is_scalar($keys)) {
222 $cache->delete($keys);
223 } else {
224 throw new coding_exception('cache_helper::invalidate_by_definition only accepts $keys as array, or scalar.');
225 }
226 return true;
227 }
228
229 /**
230 * Invalidates a given set of keys by means of an event.
231 *
232 * @todo add support for identifiers to be supplied and utilised.
233 *
234 * @param string $event
235 * @param array $keys
236 */
237 public static function invalidate_by_event($event, array $keys) {
238 $instance = cache_config::instance();
239 $invalidationeventset = false;
240 $factory = cache_factory::instance();
fb0eaa3f 241 $inuse = $factory->get_caches_in_use();
8139ad13
SH
242 foreach ($instance->get_definitions() as $name => $definitionarr) {
243 $definition = cache_definition::load($name, $definitionarr);
244 if ($definition->invalidates_on_event($event)) {
fb0eaa3f
SH
245 // First up check if there is a cache loader for this definition already.
246 // If there is we need to invalidate the keys from there.
247 $definitionkey = $definition->get_component().'/'.$definition->get_area();
248 if (isset($inuse[$definitionkey])) {
249 $inuse[$definitionkey]->delete_many($keys);
8139ad13
SH
250 }
251
fb0eaa3f
SH
252 // We should only log events for application and session caches.
253 // Request caches shouldn't have events as all data is lost at the end of the request.
254 // Events should only be logged once of course and likely several definitions are watching so we
255 // track its logging with $invalidationeventset.
256 $logevent = ($invalidationeventset === false && $definition->get_mode() !== cache_store::MODE_REQUEST);
257
258 if ($logevent) {
8139ad13
SH
259 // Get the event invalidation cache.
260 $cache = cache::make('core', 'eventinvalidation');
261 // Get any existing invalidated keys for this cache.
262 $data = $cache->get($event);
263 if ($data === false) {
264 // There are none.
265 $data = array();
266 }
267 // Add our keys to them with the current cache timestamp.
268 foreach ($keys as $key) {
269 $data[$key] = cache::now();
270 }
271 // Set that data back to the cache.
272 $cache->set($event, $data);
273 // This only needs to occur once.
274 $invalidationeventset = true;
275 }
276 }
277 }
278 }
279
280 /**
281 * Purges the cache for a specific definition.
282 *
5f5776c1
SH
283 * If you need to purge a definition that requires identifiers or an aggregate and you don't
284 * know the details of those please use cache_helper::purge_stores_used_by_definition instead.
285 * It is a more aggressive purge and will purge all data within the store, not just the data
286 * belonging to the given definition.
287 *
942be4a5
MS
288 * @todo MDL-36660: Change the signature: $aggregate must be added.
289 *
8139ad13
SH
290 * @param string $component
291 * @param string $area
292 * @param array $identifiers
293 * @return bool
294 */
295 public static function purge_by_definition($component, $area, array $identifiers = array()) {
170f821b 296 // Create the cache.
8139ad13 297 $cache = cache::make($component, $area, $identifiers);
942be4a5
MS
298 // Initialise, in case of a store.
299 if ($cache instanceof cache_store) {
300 $factory = cache_factory::instance();
301 // TODO MDL-36660: Providing $aggregate is required for purging purposes: $definition->get_id()
302 $definition = $factory->create_definition($component, $area, null);
303 $definition->set_identifiers($identifiers);
304 $cache->initialise($definition);
305 }
8139ad13
SH
306 // Purge baby, purge.
307 $cache->purge();
308 return true;
309 }
310
311 /**
312 * Purges a cache of all information on a given event.
313 *
314 * @param string $event
315 */
316 public static function purge_by_event($event) {
317 $instance = cache_config::instance();
318 $invalidationeventset = false;
319 $factory = cache_factory::instance();
fb0eaa3f 320 $inuse = $factory->get_caches_in_use();
8139ad13
SH
321 foreach ($instance->get_definitions() as $name => $definitionarr) {
322 $definition = cache_definition::load($name, $definitionarr);
323 if ($definition->invalidates_on_event($event)) {
fb0eaa3f
SH
324 // First up check if there is a cache loader for this definition already.
325 // If there is we need to invalidate the keys from there.
326 $definitionkey = $definition->get_component().'/'.$definition->get_area();
327 if (isset($inuse[$definitionkey])) {
328 $inuse[$definitionkey]->purge();
942be4a5 329 }
fb0eaa3f
SH
330
331 // We should only log events for application and session caches.
332 // Request caches shouldn't have events as all data is lost at the end of the request.
333 // Events should only be logged once of course and likely several definitions are watching so we
334 // track its logging with $invalidationeventset.
335 $logevent = ($invalidationeventset === false && $definition->get_mode() !== cache_store::MODE_REQUEST);
336
8139ad13 337 // We need to flag the event in the "Event invalidation" cache if it hasn't already happened.
fb0eaa3f 338 if ($logevent && $invalidationeventset === false) {
8139ad13
SH
339 // Get the event invalidation cache.
340 $cache = cache::make('core', 'eventinvalidation');
170f821b 341 // Create a key to invalidate all.
8139ad13
SH
342 $data = array(
343 'purged' => cache::now()
344 );
345 // Set that data back to the cache.
346 $cache->set($event, $data);
347 // This only needs to occur once.
348 $invalidationeventset = true;
349 }
350 }
351 }
352 }
353
354 /**
355 * Ensure that the stats array is ready to collect information for the given store and definition.
356 * @param string $store
357 * @param string $definition
358 */
359 protected static function ensure_ready_for_stats($store, $definition) {
8af5bb54 360 // This function is performance-sensitive, so exit as quickly as possible
361 // if we do not need to do anything.
362 if (isset(self::$stats[$definition][$store])) {
363 return;
364 }
8139ad13
SH
365 if (!array_key_exists($definition, self::$stats)) {
366 self::$stats[$definition] = array(
367 $store => array(
368 'hits' => 0,
369 'misses' => 0,
370 'sets' => 0,
371 )
372 );
373 } else if (!array_key_exists($store, self::$stats[$definition])) {
374 self::$stats[$definition][$store] = array(
375 'hits' => 0,
376 'misses' => 0,
377 'sets' => 0,
378 );
379 }
380 }
381
382 /**
383 * Record a cache hit in the stats for the given store and definition.
384 *
385 * @param string $store
386 * @param string $definition
387 */
388 public static function record_cache_hit($store, $definition) {
389 self::ensure_ready_for_stats($store, $definition);
390 self::$stats[$definition][$store]['hits']++;
391 }
392
393 /**
394 * Record a cache miss in the stats for the given store and definition.
395 *
396 * @param string $store
397 * @param string $definition
398 */
399 public static function record_cache_miss($store, $definition) {
400 self::ensure_ready_for_stats($store, $definition);
401 self::$stats[$definition][$store]['misses']++;
402 }
403
404 /**
405 * Record a cache set in the stats for the given store and definition.
406 *
407 * @param string $store
408 * @param string $definition
409 */
410 public static function record_cache_set($store, $definition) {
411 self::ensure_ready_for_stats($store, $definition);
412 self::$stats[$definition][$store]['sets']++;
413 }
414
415 /**
416 * Return the stats collected so far.
417 * @return array
418 */
419 public static function get_stats() {
420 return self::$stats;
421 }
422
423 /**
424 * Purge all of the cache stores of all of their data.
34c84c72
SH
425 *
426 * Think twice before calling this method. It will purge **ALL** caches regardless of whether they have been used recently or
427 * anything. This will involve full setup of the cache + the purge operation. On a site using caching heavily this WILL be
428 * painful.
b0dd08dd
SH
429 *
430 * @param bool $usewriter If set to true the cache_config_writer class is used. This class is special as it avoids
431 * it is still usable when caches have been disabled.
432 * Please use this option only if you really must. It's purpose is to allow the cache to be purged when it would be
433 * otherwise impossible.
8139ad13 434 */
b0dd08dd
SH
435 public static function purge_all($usewriter = false) {
436 $factory = cache_factory::instance();
437 $config = $factory->create_config_instance($usewriter);
fb8305de 438 foreach ($config->get_all_stores() as $store) {
b0dd08dd 439 self::purge_store($store['name'], $config);
8139ad13
SH
440 }
441 }
442
170f821b
SH
443 /**
444 * Purges a store given its name.
445 *
446 * @param string $storename
b0dd08dd 447 * @param cache_config $config
170f821b
SH
448 * @return bool
449 */
b0dd08dd
SH
450 public static function purge_store($storename, cache_config $config = null) {
451 if ($config === null) {
452 $config = cache_config::instance();
453 }
fb8305de
MS
454
455 $stores = $config->get_all_stores();
456 if (!array_key_exists($storename, $stores)) {
457 // The store does not exist.
458 return false;
459 }
460
461 $store = $stores[$storename];
462 $class = $store['class'];
463
464 // Found the store: is it ready?
465 $instance = new $class($store['name'], $store['configuration']);
466 if (!$instance->is_ready()) {
467 unset($instance);
468 return false;
469 }
470
471 foreach ($config->get_definitions_by_store($storename) as $id => $definition) {
472 $definition = cache_definition::load($id, $definition);
b0dd08dd
SH
473 $definitioninstance = clone($instance);
474 $definitioninstance->initialise($definition);
475 $definitioninstance->purge();
476 unset($definitioninstance);
170f821b 477 }
fb8305de
MS
478
479 return true;
170f821b
SH
480 }
481
5f5776c1
SH
482 /**
483 * Purges all of the stores used by a definition.
484 *
485 * Unlike cache_helper::purge_by_definition this purges all of the data from the stores not
486 * just the data relating to the definition.
487 * This function is useful when you must purge a definition that requires setup but you don't
488 * want to set it up.
489 *
490 * @param string $component
491 * @param string $area
492 */
493 public static function purge_stores_used_by_definition($component, $area) {
494 $factory = cache_factory::instance();
495 $config = $factory->create_config_instance();
496 $definition = $factory->create_definition($component, $area);
497 $stores = $config->get_stores_for_definition($definition);
498 foreach ($stores as $store) {
499 self::purge_store($store['name']);
500 }
501 }
502
8139ad13
SH
503 /**
504 * Returns the translated name of the definition.
505 *
506 * @param cache_definition $definition
507 * @return lang_string
508 */
509 public static function get_definition_name($definition) {
510 if ($definition instanceof cache_definition) {
511 return $definition->get_name();
512 }
513 $identifier = 'cachedef_'.clean_param($definition['area'], PARAM_STRINGID);
514 $component = $definition['component'];
515 if ($component === 'core') {
516 $component = 'cache';
517 }
518 return new lang_string($identifier, $component);
519 }
520
521 /**
47834bcd
SH
522 * Hashes a descriptive key to make it shorter and still unique.
523 * @param string|int $key
524 * @param cache_definition $definition
8139ad13
SH
525 * @return string
526 */
47834bcd
SH
527 public static function hash_key($key, cache_definition $definition) {
528 if ($definition->uses_simple_keys()) {
3d164e1e 529 if (debugging() && preg_match('#[^a-zA-Z0-9_]#', $key)) {
5dd68a75
SH
530 throw new coding_exception('Cache definition '.$definition->get_id().' requires simple keys. Invalid key provided.', $key);
531 }
702651c7
SH
532 // We put the key first so that we can be sure the start of the key changes.
533 return (string)$key . '-' . $definition->generate_single_key_prefix();
47834bcd
SH
534 }
535 $key = $definition->generate_single_key_prefix() . '-' . $key;
536 return sha1($key);
8139ad13 537 }
75af47ee
SH
538
539 /**
540 * Finds all definitions and updates them within the cache config file.
541 *
542 * @param bool $coreonly If set to true only core definitions will be updated.
543 */
544 public static function update_definitions($coreonly = false) {
545 global $CFG;
7866b094 546 // Include locallib.
75af47ee
SH
547 require_once($CFG->dirroot.'/cache/locallib.php');
548 // First update definitions
549 cache_config_writer::update_definitions($coreonly);
550 // Second reset anything we have already initialised to ensure we're all up to date.
551 cache_factory::reset();
552 }
e0d9b7c0
SH
553
554 /**
555 * Update the site identifier stored by the cache API.
556 *
557 * @param string $siteidentifier
fe86ebfa 558 * @return string The new site identifier.
e0d9b7c0
SH
559 */
560 public static function update_site_identifier($siteidentifier) {
561 global $CFG;
7866b094 562 // Include locallib.
e0d9b7c0
SH
563 require_once($CFG->dirroot.'/cache/locallib.php');
564 $factory = cache_factory::instance();
565 $factory->updating_started();
566 $config = $factory->create_config_instance(true);
fe86ebfa 567 $siteidentifier = $config->update_site_identifier($siteidentifier);
e0d9b7c0
SH
568 $factory->updating_finished();
569 cache_factory::reset();
fe86ebfa 570 return $siteidentifier;
e0d9b7c0 571 }
7634410b
SH
572
573 /**
574 * Returns the site identifier.
575 *
576 * @return string
577 */
578 public static function get_site_identifier() {
fe86ebfa
SH
579 global $CFG;
580 if (!is_null(self::$siteidentifier)) {
581 return self::$siteidentifier;
582 }
583 // If site identifier hasn't been collected yet attempt to get it from the cache config.
584 $factory = cache_factory::instance();
585 // If the factory is initialising then we don't want to try to get it from the config or we risk
586 // causing the cache to enter an infinite initialisation loop.
587 if (!$factory->is_initialising()) {
7634410b
SH
588 $config = $factory->create_config_instance();
589 self::$siteidentifier = $config->get_site_identifier();
590 }
fe86ebfa
SH
591 if (is_null(self::$siteidentifier)) {
592 // If the site identifier is still null then config isn't aware of it yet.
593 // We'll see if the CFG is loaded, and if not we will just use unknown.
594 // It's very important here that we don't use get_config. We don't want an endless cache loop!
a3b63be7 595 if (!empty($CFG->siteidentifier)) {
fe86ebfa
SH
596 self::$siteidentifier = self::update_site_identifier($CFG->siteidentifier);
597 } else {
598 // It's not being recorded in MUC's config and the config data hasn't been loaded yet.
599 // Likely we are initialising.
600 return 'unknown';
601 }
602 }
7634410b
SH
603 return self::$siteidentifier;
604 }
605
606 /**
607 * Returns the site version.
608 *
609 * @return string
610 */
611 public static function get_site_version() {
612 global $CFG;
613 return (string)$CFG->version;
614 }
059102e7
SH
615
616 /**
617 * Runs cron routines for MUC.
618 */
619 public static function cron() {
620 self::clean_old_session_data(true);
621 }
622
623 /**
624 * Cleans old session data from cache stores used for session based definitions.
625 *
626 * @param bool $output If set to true output will be given.
627 */
628 public static function clean_old_session_data($output = false) {
629 global $CFG;
630 if ($output) {
631 mtrace('Cleaning up stale session data from cache stores.');
632 }
633 $factory = cache_factory::instance();
634 $config = $factory->create_config_instance();
635 $definitions = $config->get_definitions();
636 $purgetime = time() - $CFG->sessiontimeout;
637 foreach ($definitions as $definitionarray) {
638 // We are only interested in session caches.
639 if (!($definitionarray['mode'] & cache_store::MODE_SESSION)) {
640 continue;
641 }
642 $definition = $factory->create_definition($definitionarray['component'], $definitionarray['area']);
643 $stores = $config->get_stores_for_definition($definition);
5cba0c4b
SH
644 // Turn them into store instances.
645 $stores = self::initialise_cachestore_instances($stores, $definition);
059102e7 646 // Initialise all of the stores used for that definition.
5cba0c4b 647 foreach ($stores as $store) {
059102e7
SH
648 // If the store doesn't support searching we can skip it.
649 if (!($store instanceof cache_is_searchable)) {
650 debugging('Cache stores used for session definitions should ideally be searchable.', DEBUG_DEVELOPER);
651 continue;
652 }
653 // Get all of the keys.
654 $keys = $store->find_by_prefix(cache_session::KEY_PREFIX);
655 $todelete = array();
656 foreach ($store->get_many($keys) as $key => $value) {
657 if (strpos($key, cache_session::KEY_PREFIX) !== 0 || !is_array($value) || !isset($value['lastaccess'])) {
658 continue;
659 }
660 if ((int)$value['lastaccess'] < $purgetime || true) {
661 $todelete[] = $key;
662 }
663 }
664 if (count($todelete)) {
665 $outcome = (int)$store->delete_many($todelete);
666 if ($output) {
667 $strdef = s($definition->get_id());
668 $strstore = s($store->my_name());
669 mtrace("- Removed {$outcome} old {$strdef} sessions from the '{$strstore}' cache store.");
670 }
671 }
672 }
673 }
674 }
7866b094 675}