MDL-40903 cache: fixed up event invalidation
[moodle.git] / cache / classes / helper.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  * 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  */
29 defined('MOODLE_INTERNAL') || die();
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  */
42 class cache_helper {
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();
51     /**
52      * The instance of the cache helper.
53      * @var cache_helper
54      */
55     protected static $instance;
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;
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     }
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     }
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     }
97     /**
98      * Used as a data store for initialised definitions.
99      * @var array
100      */
101     protected $definitions = array();
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();
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     }
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);
137         $stores = self::initialise_cachestore_instances($stores, $definition);
138         return $stores;
139     }
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      */
148     protected static function initialise_cachestore_instances(array $stores, cache_definition $definition) {
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     }
160     /**
161      * Returns a cache_lock instance suitable for use with the store.
162      *
163      * @param cache_store $store
164      * @return cache_lock_interface
165      */
166     public static function get_cachelock_for_store(cache_store $store) {
167         $instance = cache_config::instance();
168         $lockconf = $instance->get_lock_for_store($store->my_name());
169         $factory = cache_factory::instance();
170         return $factory->create_lock_instance($lockconf);
171     }
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             }
195             if (!is_valid_plugin_name($pluginname)) {
196                 // Better ignore plugins with problematic names here.
197                 continue;
198             }
199             $result[$pluginname] = $fulldir.'/'.$pluginname;
200             unset($item);
201         }
202         unset($items);
203         return $result;
204     }
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     }
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();
241         $inuse = $factory->get_caches_in_use();
242         foreach ($instance->get_definitions() as $name => $definitionarr) {
243             $definition = cache_definition::load($name, $definitionarr);
244             if ($definition->invalidates_on_event($event)) {
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);
250                 }
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);
258                 if ($logevent) {
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     }
280     /**
281      * Purges the cache for a specific definition.
282      *
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      *
288      * @todo MDL-36660: Change the signature: $aggregate must be added.
289      *
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()) {
296         // Create the cache.
297         $cache = cache::make($component, $area, $identifiers);
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         }
306         // Purge baby, purge.
307         $cache->purge();
308         return true;
309     }
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();
320         $inuse = $factory->get_caches_in_use();
321         foreach ($instance->get_definitions() as $name => $definitionarr) {
322             $definition = cache_definition::load($name, $definitionarr);
323             if ($definition->invalidates_on_event($event)) {
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();
329                 }
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);
337                 // We need to flag the event in the "Event invalidation" cache if it hasn't already happened.
338                 if ($logevent && $invalidationeventset === false) {
339                     // Get the event invalidation cache.
340                     $cache = cache::make('core', 'eventinvalidation');
341                     // Create a key to invalidate all.
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     }
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) {
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         }
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     }
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     }
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     }
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     }
415     /**
416      * Return the stats collected so far.
417      * @return array
418      */
419     public static function get_stats() {
420         return self::$stats;
421     }
423     /**
424      * Purge all of the cache stores of all of their data.
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.
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.
434      */
435     public static function purge_all($usewriter = false) {
436         $factory = cache_factory::instance();
437         $config = $factory->create_config_instance($usewriter);
438         foreach ($config->get_all_stores() as $store) {
439             self::purge_store($store['name'], $config);
440         }
441     }
443     /**
444      * Purges a store given its name.
445      *
446      * @param string $storename
447      * @param cache_config $config
448      * @return bool
449      */
450     public static function purge_store($storename, cache_config $config = null) {
451         if ($config === null) {
452             $config = cache_config::instance();
453         }
455         $stores = $config->get_all_stores();
456         if (!array_key_exists($storename, $stores)) {
457             // The store does not exist.
458             return false;
459         }
461         $store = $stores[$storename];
462         $class = $store['class'];
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         }
471         foreach ($config->get_definitions_by_store($storename) as $id => $definition) {
472             $definition = cache_definition::load($id, $definition);
473             $definitioninstance = clone($instance);
474             $definitioninstance->initialise($definition);
475             $definitioninstance->purge();
476             unset($definitioninstance);
477         }
479         return true;
480     }
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     }
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     }
521     /**
522      * Hashes a descriptive key to make it shorter and still unique.
523      * @param string|int $key
524      * @param cache_definition $definition
525      * @return string
526      */
527     public static function hash_key($key, cache_definition $definition) {
528         if ($definition->uses_simple_keys()) {
529             if (debugging() && preg_match('#[^a-zA-Z0-9_]#', $key)) {
530                 throw new coding_exception('Cache definition '.$definition->get_id().' requires simple keys. Invalid key provided.', $key);
531             }
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();
534         }
535         $key = $definition->generate_single_key_prefix() . '-' . $key;
536         return sha1($key);
537     }
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;
546         // Include locallib.
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     }
554     /**
555      * Update the site identifier stored by the cache API.
556      *
557      * @param string $siteidentifier
558      * @return string The new site identifier.
559      */
560     public static function update_site_identifier($siteidentifier) {
561         global $CFG;
562         // Include locallib.
563         require_once($CFG->dirroot.'/cache/locallib.php');
564         $factory = cache_factory::instance();
565         $factory->updating_started();
566         $config = $factory->create_config_instance(true);
567         $siteidentifier = $config->update_site_identifier($siteidentifier);
568         $factory->updating_finished();
569         cache_factory::reset();
570         return $siteidentifier;
571     }
573     /**
574      * Returns the site identifier.
575      *
576      * @return string
577      */
578     public static function get_site_identifier() {
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()) {
588             $config = $factory->create_config_instance();
589             self::$siteidentifier = $config->get_site_identifier();
590         }
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!
595             if (!empty($CFG->siteidentifier)) {
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         }
603         return self::$siteidentifier;
604     }
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     }
616     /**
617      * Runs cron routines for MUC.
618      */
619     public static function cron() {
620         self::clean_old_session_data(true);
621     }
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);
644             // Turn them into store instances.
645             $stores = self::initialise_cachestore_instances($stores, $definition);
646             // Initialise all of the stores used for that definition.
647             foreach ($stores as $store) {
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     }