MDL-41106 cache: several fixes for the session cache.
[moodle.git] / cache / stores / session / lib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * The library file for the session cache store.
19  *
20  * This file is part of the session cache store, it contains the API for interacting with an instance of the store.
21  * This is used as a default cache store within the Cache API. It should never be deleted.
22  *
23  * @package    cachestore_session
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 session data store class.
33  *
34  * @copyright  2012 Sam Hemelryk
35  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36  */
37 abstract class session_data_store extends cache_store {
39     /**
40      * Used for the actual storage.
41      * @var array
42      */
43     private static $sessionstore = null;
45     /**
46      * Returns a static store by reference... REFERENCE SUPER IMPORTANT.
47      *
48      * @param string $id
49      * @return array
50      */
51     protected static function &register_store_id($id) {
52         if (is_null(self::$sessionstore)) {
53             global $SESSION;
54             if (!isset($SESSION->cachestore_session)) {
55                 $SESSION->cachestore_session = array();
56             }
57             self::$sessionstore =& $SESSION->cachestore_session;
58         }
59         if (!array_key_exists($id, self::$sessionstore)) {
60             self::$sessionstore[$id] = array();
61         }
62         return self::$sessionstore[$id];
63     }
65     /**
66      * Flushes the data belong to the given store id.
67      * @param string $id
68      */
69     protected static function flush_store_by_id($id) {
70         unset(self::$sessionstore[$id]);
71         self::$sessionstore[$id] = array();
72     }
74     /**
75      * Flushes the store of all data.
76      */
77     protected static function flush_store() {
78         $ids = array_keys(self::$sessionstore);
79         unset(self::$sessionstore);
80         self::$sessionstore = array();
81         foreach ($ids as $id) {
82             self::$sessionstore[$id] = array();
83         }
84     }
85 }
87 /**
88  * The Session store class.
89  *
90  * @copyright  2012 Sam Hemelryk
91  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
92  */
93 class cachestore_session extends session_data_store implements cache_is_key_aware, cache_is_searchable {
95     /**
96      * The name of the store
97      * @var store
98      */
99     protected $name;
101     /**
102      * The store id (should be unique)
103      * @var string
104      */
105     protected $storeid;
107     /**
108      * The store we use for data.
109      * @var array
110      */
111     protected $store;
113     /**
114      * The ttl if there is one. Hopefully not.
115      * @var int
116      */
117     protected $ttl = 0;
119     /**
120      * The maximum size for the store, or false if there isn't one.
121      * @var bool|int
122      */
123     protected $maxsize = false;
125     /**
126      * The number of items currently being stored.
127      * @var int
128      */
129     protected $storecount = 0;
131     /**
132      * Constructs the store instance.
133      *
134      * Noting that this function is not an initialisation. It is used to prepare the store for use.
135      * The store will be initialised when required and will be provided with a cache_definition at that time.
136      *
137      * @param string $name
138      * @param array $configuration
139      */
140     public function __construct($name, array $configuration = array()) {
141         $this->name = $name;
142     }
144     /**
145      * Returns the supported features as a combined int.
146      *
147      * @param array $configuration
148      * @return int
149      */
150     public static function get_supported_features(array $configuration = array()) {
151         return self::SUPPORTS_DATA_GUARANTEE +
152                self::SUPPORTS_NATIVE_TTL +
153                self::IS_SEARCHABLE;
154     }
156     /**
157      * Returns false as this store does not support multiple identifiers.
158      * (This optional function is a performance optimisation; it must be
159      * consistent with the value from get_supported_features.)
160      *
161      * @return bool False
162      */
163     public function supports_multiple_identifiers() {
164         return false;
165     }
167     /**
168      * Returns the supported modes as a combined int.
169      *
170      * @param array $configuration
171      * @return int
172      */
173     public static function get_supported_modes(array $configuration = array()) {
174         return self::MODE_SESSION;
175     }
177     /**
178      * Returns true if the store requirements are met.
179      *
180      * @return bool
181      */
182     public static function are_requirements_met() {
183         return true;
184     }
186     /**
187      * Returns true if the given mode is supported by this store.
188      *
189      * @param int $mode One of cache_store::MODE_*
190      * @return bool
191      */
192     public static function is_supported_mode($mode) {
193         return ($mode === self::MODE_SESSION);
194     }
196     /**
197      * Initialises the cache.
198      *
199      * Once this has been done the cache is all set to be used.
200      *
201      * @param cache_definition $definition
202      */
203     public function initialise(cache_definition $definition) {
204         $this->storeid = $definition->generate_definition_hash();
205         $this->store = &self::register_store_id($this->name.'-'.$definition->get_id());
206         $this->ttl = $definition->get_ttl();
207         $maxsize = $definition->get_maxsize();
208         if ($maxsize !== null) {
209             // Must be a positive int.
210             $this->maxsize = abs((int)$maxsize);
211             $this->storecount = count($this->store);
212         }
213         $this->check_ttl();
214     }
216     /**
217      * Returns true once this instance has been initialised.
218      *
219      * @return bool
220      */
221     public function is_initialised() {
222         return (is_array($this->store));
223     }
225     /**
226      * Returns true if this store instance is ready to be used.
227      * @return bool
228      */
229     public function is_ready() {
230         return true;
231     }
233     /**
234      * Retrieves an item from the cache store given its key.
235      *
236      * @param string $key The key to retrieve
237      * @return mixed The data that was associated with the key, or false if the key did not exist.
238      */
239     public function get($key) {
240         if (isset($this->store[$key])) {
241             if ($this->ttl == 0) {
242                 $value = $this->store[$key][0];
243                 if ($this->maxsize !== false) {
244                     // Make sure the element is now in the end of array.
245                     $this->set($key, $value);
246                 }
247                 return $value;
248             } else if ($this->store[$key][1] >= (cache::now() - $this->ttl)) {
249                 return $this->store[$key][0];
250             } else {
251                 // Element is present but has expired.
252                 $this->check_ttl();
253             }
254         }
255         return false;
256     }
258     /**
259      * Retrieves several items from the cache store in a single transaction.
260      *
261      * 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.
262      *
263      * @param array $keys The array of keys to retrieve
264      * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will
265      *      be set to false.
266      */
267     public function get_many($keys) {
268         $return = array();
269         $maxtime = 0;
270         if ($this->ttl != 0) {
271             $maxtime = cache::now() - $this->ttl;
272         }
274         $hasexpiredelements = false;
275         foreach ($keys as $key) {
276             $return[$key] = false;
277             if (isset($this->store[$key])) {
278                 if ($this->ttl == 0) {
279                     $return[$key] = $this->store[$key][0];
280                     if ($this->maxsize !== false) {
281                         // Make sure the element is now in the end of array.
282                         $this->set($key, $return[$key], false);
283                     }
284                 } else if ($this->store[$key][1] >= $maxtime) {
285                     $return[$key] = $this->store[$key][0];
286                 } else {
287                     $hasexpiredelements = true;
288                 }
289             }
290         }
291         if ($hasexpiredelements) {
292             // There are some elements that are present but have expired.
293             $this->check_ttl();
294         }
295         return $return;
296     }
298     /**
299      * Sets an item in the cache given its key and data value.
300      *
301      * @param string $key The key to use.
302      * @param mixed $data The data to set.
303      * @param bool $testmaxsize If set to true then we test the maxsize arg and reduce if required. If this is set to false you will
304      *      need to perform these checks yourself. This allows for bulk set's to be performed and maxsize tests performed once.
305      * @return bool True if the operation was a success false otherwise.
306      */
307     public function set($key, $data, $testmaxsize = true) {
308         $testmaxsize = ($testmaxsize && $this->maxsize !== false);
309         $increment = $this->maxsize !== false && !isset($this->store[$key]);
310         if (($this->maxsize !== false && !$increment) || $this->ttl != 0) {
311             // Make sure the element is added to the end of $this->store array.
312             unset($this->store[$key]);
313         }
314         if ($this->ttl === 0) {
315             $this->store[$key] = array($data, 0);
316         } else {
317             $this->store[$key] = array($data, cache::now());
318         }
319         if ($increment) {
320             $this->storecount++;
321         }
322         if ($testmaxsize && $this->storecount > $this->maxsize) {
323             $this->reduce_for_maxsize();
324         }
325         return true;
326     }
328     /**
329      * Sets many items in the cache in a single transaction.
330      *
331      * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
332      *      keys, 'key' and 'value'.
333      * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
334      *      sent ... if they care that is.
335      */
336     public function set_many(array $keyvaluearray) {
337         $count = 0;
338         $increment = 0;
339         foreach ($keyvaluearray as $pair) {
340             $key = $pair['key'];
341             $data = $pair['value'];
342             $count++;
343             if ($this->maxsize !== false || $this->ttl !== 0) {
344                 // Make sure the element is added to the end of $this->store array.
345                 $this->delete($key);
346                 $increment++;
347             } else if (!isset($this->store[$key])) {
348                 $increment++;
349             }
350             if ($this->ttl === 0) {
351                 $this->store[$key] = array($data, 0);
352             } else {
353                 $this->store[$key] = array($data, cache::now());
354             }
355         }
356         if ($this->maxsize !== false) {
357             $this->storecount += $increment;
358             if ($this->storecount > $this->maxsize) {
359                 $this->reduce_for_maxsize();
360             }
361         }
362         return $count;
363     }
365     /**
366      * Checks if the store has a record for the given key and returns true if so.
367      *
368      * @param string $key
369      * @return bool
370      */
371     public function has($key) {
372         if (isset($this->store[$key])) {
373             if ($this->ttl == 0) {
374                 return true;
375             } else if ($this->store[$key][1] >= (cache::now() - $this->ttl)) {
376                 return true;
377             }
378         }
379         return false;
380     }
382     /**
383      * Returns true if the store contains records for all of the given keys.
384      *
385      * @param array $keys
386      * @return bool
387      */
388     public function has_all(array $keys) {
389         $maxtime = 0;
390         if ($this->ttl != 0) {
391             $maxtime = cache::now() - $this->ttl;
392         }
394         foreach ($keys as $key) {
395             if (!isset($this->store[$key])) {
396                 return false;
397             }
398             if ($this->ttl != 0 && $this->store[$key][1] < $maxtime) {
399                 return false;
400             }
401         }
402         return true;
403     }
405     /**
406      * Returns true if the store contains records for any of the given keys.
407      *
408      * @param array $keys
409      * @return bool
410      */
411     public function has_any(array $keys) {
412         $maxtime = 0;
413         if ($this->ttl != 0) {
414             $maxtime = cache::now() - $this->ttl;
415         }
417         foreach ($keys as $key) {
418             if (isset($this->store[$key]) && ($this->ttl == 0 || $this->store[$key][1] >= $maxtime)) {
419                 return true;
420             }
421         }
422         return false;
423     }
425     /**
426      * Deletes an item from the cache store.
427      *
428      * @param string $key The key to delete.
429      * @return bool Returns true if the operation was a success, false otherwise.
430      */
431     public function delete($key) {
432         if (!isset($this->store[$key])) {
433             return false;
434         }
435         unset($this->store[$key]);
436         if ($this->maxsize !== false) {
437             $this->storecount--;
438         }
439         return true;
440     }
442     /**
443      * Deletes several keys from the cache in a single action.
444      *
445      * @param array $keys The keys to delete
446      * @return int The number of items successfully deleted.
447      */
448     public function delete_many(array $keys) {
449         // The number of items that have actually being removed.
450         $reduction = 0;
451         foreach ($keys as $key) {
452             if (isset($this->store[$key])) {
453                 $reduction++;
454             }
455             unset($this->store[$key]);
456         }
457         if ($this->maxsize !== false) {
458             $this->storecount -= $reduction;
459         }
460         return $reduction;
461     }
463     /**
464      * Purges the cache deleting all items within it.
465      *
466      * @return boolean True on success. False otherwise.
467      */
468     public function purge() {
469         $this->store = array();
470         // Don't worry about checking if we're using max size just set it as thats as fast as the check.
471         $this->storecount = 0;
472         return true;
473     }
475     /**
476      * Reduces the size of the array if maxsize has been hit.
477      *
478      * This function reduces the size of the store reducing it by 10% of its maxsize.
479      * It removes the oldest items in the store when doing this.
480      * The reason it does this an doesn't use a least recently used system is purely the overhead such a system
481      * requires. The current approach is focused on speed, MUC already adds enough overhead to static/session caches
482      * and avoiding more is of benefit.
483      *
484      * @return int
485      */
486     protected function reduce_for_maxsize() {
487         $diff = $this->storecount - $this->maxsize;
488         if ($diff < 1) {
489             return 0;
490         }
491         // Reduce it by an extra 10% to avoid calling this repetitively if we are in a loop.
492         $diff += floor($this->maxsize / 10);
493         $this->store = array_slice($this->store, $diff, null, true);
494         $this->storecount -= $diff;
495         return $diff;
496     }
498     /**
499      * Returns true if the user can add an instance of the store plugin.
500      *
501      * @return bool
502      */
503     public static function can_add_instance() {
504         return false;
505     }
507     /**
508      * Performs any necessary clean up when the store instance is being deleted.
509      */
510     public function instance_deleted() {
511         $this->purge();
512     }
514     /**
515      * Generates an instance of the cache store that can be used for testing.
516      *
517      * @param cache_definition $definition
518      * @return cachestore_session
519      */
520     public static function initialise_test_instance(cache_definition $definition) {
521         // Do something here perhaps.
522         $cache = new cachestore_session('Session test');
523         $cache->initialise($definition);
524         return $cache;
525     }
527     /**
528      * Returns the name of this instance.
529      * @return string
530      */
531     public function my_name() {
532         return $this->name;
533     }
535     /**
536      * Removes expired elements.
537      * @return int number of removed elements
538      */
539     protected function check_ttl() {
540         if ($this->ttl === 0) {
541             return 0;
542         }
543         $maxtime = cache::now() - $this->ttl;
544         $count = 0;
545         for ($value = reset($this->store); $value !== false; $value = next($this->store)) {
546             if ($value[1] >= $maxtime) {
547                 // We know that elements are sorted by ttl so no need to continue.
548                 break;
549             }
550             $count++;
551         }
552         if ($count) {
553             // Remove first $count elements as they are expired.
554             $this->store = array_slice($this->store, $count, null, true);
555             if ($this->maxsize !== false) {
556                 $this->storecount -= $count;
557             }
558         }
559         return $count;
560     }
562     /**
563      * Finds all of the keys being stored in the cache store instance.
564      *
565      * @return array
566      */
567     public function find_all() {
568         $this->check_ttl();
569         return array_keys($this->store);
570     }
572     /**
573      * Finds all of the keys whose keys start with the given prefix.
574      *
575      * @param string $prefix
576      * @return array An array of keys.
577      */
578     public function find_by_prefix($prefix) {
579         $return = array();
580         foreach ($this->find_all() as $key) {
581             if (strpos($key, $prefix) === 0) {
582                 $return[] = $key;
583             }
584         }
585         return $return;
586     }
588     /**
589      * This store supports native TTL handling.
590      * @return bool
591      */
592     public function store_supports_native_ttl() {
593         return true;
594     }