Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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 loaders
19
 *
20
 * This file is part of Moodle's cache API, affectionately called MUC.
21
 * It contains the components that are required 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
 
29
defined('MOODLE_INTERNAL') || die();
30
 
31
/**
32
 * The main cache class.
33
 *
34
 * This class if the first class that any end developer will interact with.
35
 * In order to create an instance of a cache that they can work with they must call one of the static make methods belonging
36
 * to this class.
37
 *
38
 * @package    core
39
 * @category   cache
40
 * @copyright  2012 Sam Hemelryk
41
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42
 */
43
class cache implements cache_loader {
44
 
45
    /**
46
     * @var int Constant for cache entries that do not have a version number
47
     */
48
    const VERSION_NONE = -1;
49
 
50
    /**
51
     * We need a timestamp to use within the cache API.
52
     * This stamp needs to be used for all ttl and time based operations to ensure that we don't end up with
53
     * timing issues.
54
     * @var int
55
     */
56
    protected static $now;
57
 
58
    /**
59
     * A purge token used to distinguish between multiple cache purges in the same second.
60
     * This is in the format <microtime>-<random string>.
61
     *
62
     * @var string
63
     */
64
    protected static $purgetoken;
65
 
66
    /**
67
     * The definition used when loading this cache if there was one.
68
     * @var cache_definition
69
     */
70
    private $definition = false;
71
 
72
    /**
73
     * The cache store that this loader will make use of.
74
     * @var cache_store
75
     */
76
    private $store;
77
 
78
    /**
79
     * The next cache loader in the chain if there is one.
80
     * If a cache request misses for the store belonging to this loader then the loader
81
     * stored here will be checked next.
82
     * If there is a loader here then $datasource must be false.
83
     * @var cache_loader|false
84
     */
85
    private $loader = false;
86
 
87
    /**
88
     * The data source to use if we need to load data (because if doesn't exist in the cache store).
89
     * If there is a data source here then $loader above must be false.
90
     * @var cache_data_source|false
91
     */
92
    private $datasource = false;
93
 
94
    /**
95
     * Used to quickly check if the store supports key awareness.
96
     * This is set when the cache is initialised and is used to speed up processing.
97
     * @var bool
98
     */
99
    private $supportskeyawareness = null;
100
 
101
    /**
102
     * Used to quickly check if the store supports ttl natively.
103
     * This is set when the cache is initialised and is used to speed up processing.
104
     * @var bool
105
     */
106
    private $supportsnativettl = null;
107
 
108
    /**
109
     * Gets set to true if the cache is going to be using a static array for acceleration.
110
     * The array statically caches items used during the lifetime of the request. This greatly speeds up interaction
111
     * with the cache in areas where it will be repetitively hit for the same information such as with strings.
112
     * There are several other variables to control how this static acceleration array works.
113
     * @var bool
114
     */
115
    private $staticacceleration = false;
116
 
117
    /**
118
     * The static acceleration array.
119
     * Items will be stored in this cache as they were provided. This ensure there is no unnecessary processing taking place.
120
     * @var array
121
     */
122
    private $staticaccelerationarray = array();
123
 
124
    /**
125
     * The number of items in the static acceleration array. Avoids count calls like you wouldn't believe.
126
     * @var int
127
     */
128
    private $staticaccelerationcount = 0;
129
 
130
    /**
131
     * An array containing just the keys being used in the static acceleration array.
132
     * This seems redundant perhaps but is used when managing the size of the static acceleration array.
133
     * Items are added to the end of the array and the when we need to reduce the size of the cache we use the
134
     * key that is first on this array.
135
     * @var array
136
     */
137
    private $staticaccelerationkeys = array();
138
 
139
    /**
140
     * The maximum size of the static acceleration array.
141
     *
142
     * If set to false there is no max size.
143
     * Caches that make use of static acceleration should seriously consider setting this to something reasonably small, but
144
     * still large enough to offset repetitive calls.
145
     *
146
     * @var int|false
147
     */
148
    private $staticaccelerationsize = false;
149
 
150
    /**
151
     * Gets set to true during initialisation if the definition is making use of a ttl.
152
     * Used to speed up processing.
153
     * @var bool
154
     */
155
    private $hasattl = false;
156
 
157
    /**
158
     * Gets set to the class name of the store during initialisation. This is used several times in the cache class internally
159
     * and having it here helps speed up processing.
160
     * @var strubg
161
     */
162
    protected $storetype = 'unknown';
163
 
164
    /**
165
     * Gets set to true if we want to collect performance information about the cache API.
166
     * @var bool
167
     */
168
    protected $perfdebug = false;
169
 
170
    /**
171
     * Determines if this loader is a sub loader, not the top of the chain.
172
     * @var bool
173
     */
174
    protected $subloader = false;
175
 
176
    /**
177
     * Gets set to true if the cache writes (set|delete) must have a manual lock created first.
178
     * @var bool
179
     */
180
    protected $requirelockingbeforewrite = false;
181
 
182
    /**
183
     * Gets set to true if the cache's primary store natively supports locking.
184
     * If it does then we use that, otherwise we need to instantiate a second store to use for locking.
185
     * @var cache_store|null
186
     */
187
    protected $nativelocking = null;
188
 
189
    /**
190
     * Creates a new cache instance for a pre-defined definition.
191
     *
192
     * @param string $component The component for the definition
193
     * @param string $area The area for the definition
194
     * @param array $identifiers Any additional identifiers that should be provided to the definition.
195
     * @param string $unused Used to be datasourceaggregate but that was removed and this is now unused.
196
     * @return cache_application|cache_session|cache_store
197
     */
198
    public static function make($component, $area, array $identifiers = array(), $unused = null) {
199
        $factory = cache_factory::instance();
200
        return $factory->create_cache_from_definition($component, $area, $identifiers);
201
    }
202
 
203
    /**
204
     * Creates a new cache instance based upon the given params.
205
     *
206
     * @param int $mode One of cache_store::MODE_*
207
     * @param string $component The component this cache relates to.
208
     * @param string $area The area this cache relates to.
209
     * @param array $identifiers Any additional identifiers that should be provided to the definition.
210
     * @param array $options An array of options, available options are:
211
     *   - simplekeys : Set to true if the keys you will use are a-zA-Z0-9_
212
     *   - simpledata : Set to true if the type of the data you are going to store is scalar, or an array of scalar vars
213
     *   - staticacceleration : If set to true the cache will hold onto data passing through it.
214
     *   - staticaccelerationsize : The max size for the static acceleration array.
215
     * @return cache_application|cache_session|cache_request
216
     */
217
    public static function make_from_params($mode, $component, $area, array $identifiers = array(), array $options = array()) {
218
        $factory = cache_factory::instance();
219
        return $factory->create_cache_from_params($mode, $component, $area, $identifiers, $options);
220
    }
221
 
222
    /**
223
     * Constructs a new cache instance.
224
     *
225
     * You should not call this method from your code, instead you should use the cache::make methods.
226
     *
227
     * This method is public so that the cache_factory is able to instantiate cache instances.
228
     * Ideally we would make this method protected and expose its construction to the factory method internally somehow.
229
     * The factory class is responsible for this in order to centralise the storage of instances once created. This way if needed
230
     * we can force a reset of the cache API (used during unit testing).
231
     *
232
     * @param cache_definition $definition The definition for the cache instance.
233
     * @param cache_store $store The store that cache should use.
234
     * @param cache_loader|cache_data_source $loader The next loader in the chain or the data source if there is one and there
235
     *      are no other cache_loaders in the chain.
236
     */
237
    public function __construct(cache_definition $definition, cache_store $store, $loader = null) {
238
        global $CFG;
239
        $this->definition = $definition;
240
        $this->store = $store;
241
        $this->storetype = get_class($store);
242
        $this->perfdebug = (!empty($CFG->perfdebug) and $CFG->perfdebug > 7);
243
        if ($loader instanceof cache_loader) {
244
            $this->set_loader($loader);
245
        } else if ($loader instanceof cache_data_source) {
246
            $this->set_data_source($loader);
247
        }
248
        $this->definition->generate_definition_hash();
249
        $this->staticacceleration = $this->definition->use_static_acceleration();
250
        if ($this->staticacceleration) {
251
            $this->staticaccelerationsize = $this->definition->get_static_acceleration_size();
252
        }
253
        $this->hasattl = ($this->definition->get_ttl() > 0);
254
    }
255
 
256
    /**
257
     * Set the loader for this cache.
258
     *
259
     * @param   cache_loader $loader
260
     */
261
    protected function set_loader(cache_loader $loader): void {
262
        $this->loader = $loader;
263
 
264
        // Mark the loader as a sub (chained) loader.
265
        $this->loader->set_is_sub_loader(true);
266
    }
267
 
268
    /**
269
     * Set the data source for this cache.
270
     *
271
     * @param   cache_data_source $datasource
272
     */
273
    protected function set_data_source(cache_data_source $datasource): void {
274
        $this->datasource = $datasource;
275
    }
276
 
277
    /**
278
     * Used to inform the loader of its state as a sub loader, or as the top of the chain.
279
     *
280
     * This is important as it ensures that we do not have more than one loader keeping static acceleration data.
281
     * Subloaders need to be "pure" loaders in the sense that they are used to store and retrieve information from stores or the
282
     * next loader/data source in the chain.
283
     * Nothing fancy, nothing flash.
284
     *
285
     * @param bool $setting
286
     */
287
    protected function set_is_sub_loader($setting = true) {
288
        if ($setting) {
289
            $this->subloader = true;
290
            // Subloaders should not keep static acceleration data.
291
            $this->staticacceleration = false;
292
            $this->staticaccelerationsize = false;
293
        } else {
294
            $this->subloader = true;
295
            $this->staticacceleration = $this->definition->use_static_acceleration();
296
            if ($this->staticacceleration) {
297
                $this->staticaccelerationsize = $this->definition->get_static_acceleration_size();
298
            }
299
        }
300
    }
301
 
302
    /**
303
     * Alters the identifiers that have been provided to the definition.
304
     *
305
     * This is an advanced method and should not be used unless really needed.
306
     * It allows the developer to slightly alter the definition without having to re-establish the cache.
307
     * It will cause more processing as the definition will need to clear and reprepare some of its properties.
308
     *
309
     * @param array $identifiers
310
     */
311
    public function set_identifiers(array $identifiers) {
312
        if ($this->definition->set_identifiers($identifiers)) {
313
            // As static acceleration uses input keys and not parsed keys
314
            // it much be cleared when the identifier set is changed.
315
            $this->staticaccelerationarray = array();
316
            if ($this->staticaccelerationsize !== false) {
317
                $this->staticaccelerationkeys = array();
318
                $this->staticaccelerationcount = 0;
319
            }
320
        }
321
    }
322
 
323
    /**
324
     * Process any outstanding invalidation events for the cache we are registering,
325
     *
326
     * Identifiers and event invalidation are not compatible with each other at this time.
327
     * As a result the cache does not need to consider identifiers when working out what to invalidate.
328
     */
329
    protected function handle_invalidation_events() {
330
        if (!$this->definition->has_invalidation_events()) {
331
            return;
332
        }
333
 
334
        // Each cache stores the current 'lastinvalidation' value within the cache itself.
335
        $lastinvalidation = $this->get('lastinvalidation');
336
        if ($lastinvalidation === false) {
337
            // There is currently no  value for the lastinvalidation token, therefore the token is not set, and there
338
            // can be nothing to invalidate.
339
            // Set the lastinvalidation value to the current purge token and return early.
340
            $this->set('lastinvalidation', self::get_purge_token());
341
            return;
342
        } else if ($lastinvalidation == self::get_purge_token()) {
343
            // The current purge request has already been fully handled by this cache.
344
            return;
345
        }
346
 
347
        /*
348
         * Now that the whole cache check is complete, we check the meaning of any specific cache invalidation events.
349
         * These are stored in the core/eventinvalidation cache as an multi-dimensinoal array in the form:
350
         *  [
351
         *      eventname => [
352
         *          keyname => purgetoken,
353
         *      ]
354
         *  ]
355
         *
356
         * The 'keyname' value is used to delete a specific key in the cache.
357
         * If the keyname is set to the special value 'purged', then the whole cache is purged instead.
358
         *
359
         * The 'purgetoken' is the token that this key was last purged.
360
         * a) If the purgetoken matches the last invalidation, then the key/cache is not purged.
361
         * b) If the purgetoken is newer than the last invalidation, then the key/cache is not purged.
362
         * c) If the purge token is older than the last invalidation, or it has a different token component, then the
363
         *    cache is purged.
364
         *
365
         * Option b should not happen under normal operation, but may happen in race condition whereby a long-running
366
         * request's cache is cleared in another process during that request, and prior to that long-running request
367
         * creating the cache. In such a condition, it would be incorrect to clear that cache.
368
         */
369
        $cache = self::make('core', 'eventinvalidation');
370
        $events = $cache->get_many($this->definition->get_invalidation_events());
371
        $todelete = array();
372
        $purgeall = false;
373
 
374
        // Iterate the returned data for the events.
375
        foreach ($events as $event => $keys) {
376
            if ($keys === false) {
377
                // No data to be invalidated yet.
378
                continue;
379
            }
380
 
381
            // Look at each key and check the timestamp.
382
            foreach ($keys as $key => $purgetoken) {
383
                // If the timestamp of the event is more than or equal to the last invalidation (happened between the last
384
                // invalidation and now), then we need to invaliate the key.
385
                if (self::compare_purge_tokens($purgetoken, $lastinvalidation) > 0) {
386
                    if ($key === 'purged') {
387
                        $purgeall = true;
388
                        break;
389
                    } else {
390
                        $todelete[] = $key;
391
                    }
392
                }
393
            }
394
        }
395
        if ($purgeall) {
396
            $this->purge();
397
        } else if (!empty($todelete)) {
398
            $todelete = array_unique($todelete);
399
            $this->delete_many($todelete);
400
        }
401
        // Set the time of the last invalidation.
402
        if ($purgeall || !empty($todelete)) {
403
            $this->set('lastinvalidation', self::get_purge_token(true));
404
        }
405
    }
406
 
407
    /**
408
     * Retrieves the value for the given key from the cache.
409
     *
410
     * @param string|int $key The key for the data being requested.
411
     *      It can be any structure although using a scalar string or int is recommended in the interests of performance.
412
     *      In advanced cases an array may be useful such as in situations requiring the multi-key functionality.
413
     * @param int $strictness One of IGNORE_MISSING | MUST_EXIST
414
     * @return mixed|false The data from the cache or false if the key did not exist within the cache.
415
     * @throws coding_exception
416
     */
417
    public function get($key, $strictness = IGNORE_MISSING) {
418
        return $this->get_implementation($key, self::VERSION_NONE, $strictness);
419
    }
420
 
421
    /**
422
     * Retrieves the value and actual version for the given key, with at least the required version.
423
     *
424
     * If there is no value for the key, or there is a value but it doesn't have the required
425
     * version, then this function will return null (or throw an exception if you set strictness
426
     * to MUST_EXIST).
427
     *
428
     * This function can be used to make it easier to support localisable caches (where the cache
429
     * could be stored on a local server as well as a shared cache). Specifying the version means
430
     * that it will automatically retrieve the correct version if available, either from the local
431
     * server or [if that has an older version] from the shared server.
432
     *
433
     * If the cached version is newer than specified version, it will be returned regardless. For
434
     * example, if you request version 4, but the locally cached version is 5, it will be returned.
435
     * If you request version 6, and the locally cached version is 5, then the system will look in
436
     * higher-level caches (if any); if there still isn't a version 6 or greater, it will return
437
     * null.
438
     *
439
     * You must use this function if you use set_versioned.
440
     *
441
     * @param string|int $key The key for the data being requested.
442
     * @param int $requiredversion Minimum required version of the data
443
     * @param int $strictness One of IGNORE_MISSING or MUST_EXIST.
444
     * @param mixed $actualversion If specified, will be set to the actual version number retrieved
445
     * @return mixed Data from the cache, or false if the key did not exist or was too old
446
     * @throws \coding_exception If you call get_versioned on a non-versioned cache key
447
     */
448
    public function get_versioned($key, int $requiredversion, int $strictness = IGNORE_MISSING, &$actualversion = null) {
449
        return $this->get_implementation($key, $requiredversion, $strictness, $actualversion);
450
    }
451
 
452
    /**
453
     * Checks returned data to see if it matches the specified version number.
454
     *
455
     * For versioned data, this returns the version_wrapper object (or false). For other
456
     * data, it returns the actual data (or false).
457
     *
458
     * @param mixed $result Result data
459
     * @param int $requiredversion Required version number or VERSION_NONE if there must be no version
460
     * @return bool True if version is current, false if not (or if result is false)
461
     * @throws \coding_exception If unexpected type of data (versioned vs non-versioned) is found
462
     */
463
    protected static function check_version($result, int $requiredversion): bool {
464
        if ($requiredversion === self::VERSION_NONE) {
465
            if ($result instanceof \core_cache\version_wrapper) {
466
                throw new \coding_exception('Unexpectedly found versioned cache entry');
467
            } else {
468
                // No version checks, so version is always correct.
469
                return true;
470
            }
471
        } else {
472
            // If there's no result, obviously it doesn't meet the required version.
473
            if (!cache_helper::result_found($result)) {
474
                return false;
475
            }
476
            if (!($result instanceof \core_cache\version_wrapper)) {
477
                throw new \coding_exception('Unexpectedly found non-versioned cache entry');
478
            }
479
            // If the result doesn't match the required version tag, return false.
480
            if ($result->version < $requiredversion) {
481
                return false;
482
            }
483
            // The version meets the requirement.
484
            return true;
485
        }
486
    }
487
 
488
    /**
489
     * Retrieves the value for the given key from the cache.
490
     *
491
     * @param string|int $key The key for the data being requested.
492
     *      It can be any structure although using a scalar string or int is recommended in the interests of performance.
493
     *      In advanced cases an array may be useful such as in situations requiring the multi-key functionality.
494
     * @param int $requiredversion Minimum required version of the data or cache::VERSION_NONE
495
     * @param int $strictness One of IGNORE_MISSING | MUST_EXIST
496
     * @param mixed $actualversion If specified, will be set to the actual version number retrieved
497
     * @return mixed|false The data from the cache or false if the key did not exist within the cache.
498
     * @throws coding_exception
499
     */
500
    protected function get_implementation($key, int $requiredversion, int $strictness, &$actualversion = null) {
501
        // 1. Get it from the static acceleration array if we can (only when it is enabled and it has already been requested/set).
502
        $usesstaticacceleration = $this->use_static_acceleration();
503
 
504
        if ($usesstaticacceleration) {
505
            $result = $this->static_acceleration_get($key);
506
            if (cache_helper::result_found($result) && self::check_version($result, $requiredversion)) {
507
                if ($requiredversion === self::VERSION_NONE) {
508
                    return $result;
509
                } else {
510
                    $actualversion = $result->version;
511
                    return $result->data;
512
                }
513
            }
514
        }
515
 
516
        // 2. Parse the key.
517
        $parsedkey = $this->parse_key($key);
518
 
519
        // 3. Get it from the store. Obviously wasn't in the static acceleration array.
520
        $result = $this->store->get($parsedkey);
521
        if (cache_helper::result_found($result)) {
522
            // Check the result has at least the required version.
523
            try {
524
                $validversion = self::check_version($result, $requiredversion);
525
            } catch (\coding_exception $e) {
526
                // In certain circumstances this could happen before users are taken to the upgrade
527
                // screen when upgrading from an earlier Moodle version that didn't use a versioned
528
                // cache for this item, so redirect instead of showing error if that's the case.
529
                redirect_if_major_upgrade_required();
530
 
531
                // If we still get an exception because there is incorrect data in the cache (not
532
                // versioned when it ought to be), delete it so this exception goes away next time.
533
                // The exception should only happen if there is a code bug (which is why we still
534
                // throw it) but there are unusual scenarios in development where it might happen
535
                // and that would be annoying if it doesn't fix itself.
536
                $this->store->delete($parsedkey);
537
                throw $e;
538
            }
539
 
540
            if (!$validversion) {
541
                // If the result was too old, don't use it.
542
                $result = false;
543
 
544
                // Also delete it immediately. This improves performance in the
545
                // case when the cache item is large and there may be multiple clients simultaneously
546
                // requesting it - they won't all have to do a megabyte of IO just in order to find
547
                // that it's out of date.
548
                $this->store->delete($parsedkey);
549
            }
550
        }
551
        if (cache_helper::result_found($result)) {
552
            // Look to see if there's a TTL wrapper. It might be inside a version wrapper.
553
            if ($requiredversion !== self::VERSION_NONE) {
554
                $ttlconsider = $result->data;
555
            } else {
556
                $ttlconsider = $result;
557
            }
558
            if ($ttlconsider instanceof cache_ttl_wrapper) {
559
                if ($ttlconsider->has_expired()) {
560
                    $this->store->delete($parsedkey);
561
                    $result = false;
562
                } else if ($requiredversion === self::VERSION_NONE) {
563
                    // Use the data inside the TTL wrapper as the result.
564
                    $result = $ttlconsider->data;
565
                } else {
566
                    // Put the data from the TTL wrapper directly inside the version wrapper.
567
                    $result->data = $ttlconsider->data;
568
                }
569
            }
570
            if ($usesstaticacceleration) {
571
                $this->static_acceleration_set($key, $result);
572
            }
573
            // Remove version wrapper if necessary.
574
            if ($requiredversion !== self::VERSION_NONE) {
575
                $actualversion = $result->version;
576
                $result = $result->data;
577
            }
578
            if ($result instanceof cache_cached_object) {
579
                $result = $result->restore_object();
580
            }
581
        }
582
 
583
        // 4. Load if from the loader/datasource if we don't already have it.
584
        $setaftervalidation = false;
585
        if (!cache_helper::result_found($result)) {
586
            if ($this->perfdebug) {
587
                cache_helper::record_cache_miss($this->store, $this->definition);
588
            }
589
            if ($this->loader !== false) {
590
                // We must pass the original (unparsed) key to the next loader in the chain.
591
                // The next loader will parse the key as it sees fit. It may be parsed differently
592
                // depending upon the capabilities of the store associated with the loader.
593
                if ($requiredversion === self::VERSION_NONE) {
594
                    $result = $this->loader->get($key);
595
                } else {
596
                    $result = $this->loader->get_versioned($key, $requiredversion, IGNORE_MISSING, $actualversion);
597
                }
598
            } else if ($this->datasource !== false) {
599
                if ($requiredversion === self::VERSION_NONE) {
600
                    $result = $this->datasource->load_for_cache($key);
601
                } else {
602
                    if (!$this->datasource instanceof cache_data_source_versionable) {
603
                        throw new \coding_exception('Data source is not versionable');
604
                    }
605
                    $result = $this->datasource->load_for_cache_versioned($key, $requiredversion, $actualversion);
606
                    if ($result && $actualversion < $requiredversion) {
607
                        throw new \coding_exception('Data source returned outdated version');
608
                    }
609
                }
610
            }
611
            $setaftervalidation = (cache_helper::result_found($result));
612
        } else if ($this->perfdebug) {
613
            $readbytes = $this->store->get_last_io_bytes();
614
            cache_helper::record_cache_hit($this->store, $this->definition, 1, $readbytes);
615
        }
616
        // 5. Validate strictness.
617
        if ($strictness === MUST_EXIST && !cache_helper::result_found($result)) {
618
            throw new coding_exception('Requested key did not exist in any cache stores and could not be loaded.');
619
        }
620
        // 6. Set it to the store if we got it from the loader/datasource. Only set to this direct
621
        // store; parent method will have set it to all stores if needed.
622
        if ($setaftervalidation) {
623
            $lock = false;
624
            try {
625
                // Only try to acquire a lock for this cache if we do not already have one.
626
                if (!empty($this->requirelockingbeforewrite) && !$this->check_lock_state($key)) {
627
                    $this->acquire_lock($key);
628
                    $lock = true;
629
                }
630
                if ($requiredversion === self::VERSION_NONE) {
631
                    $this->set_implementation($key, self::VERSION_NONE, $result, false);
632
                } else {
633
                    $this->set_implementation($key, $actualversion, $result, false);
634
                }
635
            } finally {
636
                if ($lock) {
637
                    $this->release_lock($key);
638
                }
639
            }
640
        }
641
        // 7. Make sure we don't pass back anything that could be a reference.
642
        //    We don't want people modifying the data in the cache.
643
        if (!$this->store->supports_dereferencing_objects() && !is_scalar($result)) {
644
            // If data is an object it will be a reference.
645
            // If data is an array if may contain references.
646
            // We want to break references so that the cache cannot be modified outside of itself.
647
            // Call the function to unreference it (in the best way possible).
648
            $result = $this->unref($result);
649
        }
650
        return $result;
651
    }
652
 
653
    /**
654
     * Retrieves an array of values for an array of keys.
655
     *
656
     * Using this function comes with potential performance implications.
657
     * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call
658
     * the equivalent singular method for each item provided.
659
     * This should not deter you from using this function as there is a performance benefit in situations where the cache store
660
     * does support it, but you should be aware of this fact.
661
     *
662
     * @param array $keys The keys of the data being requested.
663
     *      Each key can be any structure although using a scalar string or int is recommended in the interests of performance.
664
     *      In advanced cases an array may be useful such as in situations requiring the multi-key functionality.
665
     * @param int $strictness One of IGNORE_MISSING or MUST_EXIST.
666
     * @return array An array of key value pairs for the items that could be retrieved from the cache.
667
     *      If MUST_EXIST was used and not all keys existed within the cache then an exception will be thrown.
668
     *      Otherwise any key that did not exist will have a data value of false within the results.
669
     * @throws coding_exception
670
     */
671
    public function get_many(array $keys, $strictness = IGNORE_MISSING) {
672
 
673
        $keysparsed = array();
674
        $parsedkeys = array();
675
        $resultpersist = array();
676
        $resultstore = array();
677
        $keystofind = array();
678
        $readbytes = cache_store::IO_BYTES_NOT_SUPPORTED;
679
 
680
        // First up check the persist cache for each key.
681
        $isusingpersist = $this->use_static_acceleration();
682
        foreach ($keys as $key) {
683
            $pkey = $this->parse_key($key);
684
            if (is_array($pkey)) {
685
                $pkey = $pkey['key'];
686
            }
687
            $keysparsed[$key] = $pkey;
688
            $parsedkeys[$pkey] = $key;
689
            $keystofind[$pkey] = $key;
690
            if ($isusingpersist) {
691
                $value = $this->static_acceleration_get($key);
692
                if ($value !== false) {
693
                    $resultpersist[$pkey] = $value;
694
                    unset($keystofind[$pkey]);
695
                }
696
            }
697
        }
698
 
699
        // Next assuming we didn't find all of the keys in the persist cache try loading them from the store.
700
        if (count($keystofind)) {
701
            $resultstore = $this->store->get_many(array_keys($keystofind));
702
            if ($this->perfdebug) {
703
                $readbytes = $this->store->get_last_io_bytes();
704
            }
705
            // Process each item in the result to "unwrap" it.
706
            foreach ($resultstore as $key => $value) {
707
                if ($value instanceof cache_ttl_wrapper) {
708
                    if ($value->has_expired()) {
709
                        $value = false;
710
                    } else {
711
                        $value = $value->data;
712
                    }
713
                }
714
                if ($value !== false && $this->use_static_acceleration()) {
715
                    $this->static_acceleration_set($keystofind[$key], $value);
716
                }
717
                if ($value instanceof cache_cached_object) {
718
                    $value = $value->restore_object();
719
                }
720
                $resultstore[$key] = $value;
721
            }
722
        }
723
 
724
        // Merge the result from the persis cache with the results from the store load.
725
        $result = $resultpersist + $resultstore;
726
        unset($resultpersist);
727
        unset($resultstore);
728
 
729
        // Next we need to find any missing values and load them from the loader/datasource next in the chain.
730
        $usingloader = ($this->loader !== false);
731
        $usingsource = (!$usingloader && ($this->datasource !== false));
732
        if ($usingloader || $usingsource) {
733
            $missingkeys = array();
734
            foreach ($result as $key => $value) {
735
                if ($value === false) {
736
                    $missingkeys[] = $parsedkeys[$key];
737
                }
738
            }
739
            if (!empty($missingkeys)) {
740
                if ($usingloader) {
741
                    $resultmissing = $this->loader->get_many($missingkeys);
742
                } else {
743
                    $resultmissing = $this->datasource->load_many_for_cache($missingkeys);
744
                }
745
                foreach ($resultmissing as $key => $value) {
746
                    $result[$keysparsed[$key]] = $value;
747
                    $lock = false;
748
                    try {
749
                        if (!empty($this->requirelockingbeforewrite)) {
750
                            $this->acquire_lock($key);
751
                            $lock = true;
752
                        }
753
                        if ($value !== false) {
754
                            $this->set($key, $value);
755
                        }
756
                    } finally {
757
                        if ($lock) {
758
                            $this->release_lock($key);
759
                        }
760
                    }
761
                }
762
                unset($resultmissing);
763
            }
764
            unset($missingkeys);
765
        }
766
 
767
        // Create an array with the original keys and the found values. This will be what we return.
768
        $fullresult = array();
769
        foreach ($result as $key => $value) {
770
            if (!is_scalar($value)) {
771
                // If data is an object it will be a reference.
772
                // If data is an array if may contain references.
773
                // We want to break references so that the cache cannot be modified outside of itself.
774
                // Call the function to unreference it (in the best way possible).
775
                $value = $this->unref($value);
776
            }
777
            $fullresult[$parsedkeys[$key]] = $value;
778
        }
779
        unset($result);
780
 
781
        // Final step is to check strictness.
782
        if ($strictness === MUST_EXIST) {
783
            foreach ($keys as $key) {
784
                if (!array_key_exists($key, $fullresult)) {
785
                    throw new coding_exception('Not all the requested keys existed within the cache stores.');
786
                }
787
            }
788
        }
789
 
790
        if ($this->perfdebug) {
791
            $hits = 0;
792
            $misses = 0;
793
            foreach ($fullresult as $value) {
794
                if ($value === false) {
795
                    $misses++;
796
                } else {
797
                    $hits++;
798
                }
799
            }
800
            cache_helper::record_cache_hit($this->store, $this->definition, $hits, $readbytes);
801
            cache_helper::record_cache_miss($this->store, $this->definition, $misses);
802
        }
803
 
804
        // Return the result. Phew!
805
        return $fullresult;
806
    }
807
 
808
    /**
809
     * Sends a key => value pair to the cache.
810
     *
811
     * <code>
812
     * // This code will add four entries to the cache, one for each url.
813
     * $cache->set('main', 'http://moodle.org');
814
     * $cache->set('docs', 'http://docs.moodle.org');
815
     * $cache->set('tracker', 'http://tracker.moodle.org');
816
     * $cache->set('qa', 'http://qa.moodle.net');
817
     * </code>
818
     *
819
     * @param string|int $key The key for the data being requested.
820
     *      It can be any structure although using a scalar string or int is recommended in the interests of performance.
821
     *      In advanced cases an array may be useful such as in situations requiring the multi-key functionality.
822
     * @param mixed $data The data to set against the key.
823
     * @return bool True on success, false otherwise.
824
     */
825
    public function set($key, $data) {
826
        return $this->set_implementation($key, self::VERSION_NONE, $data);
827
    }
828
 
829
    /**
830
     * Sets the value for the given key with the given version.
831
     *
832
     * The cache does not store multiple versions - any existing version will be overwritten with
833
     * this one. This function should only be used if there is a known 'current version' (e.g.
834
     * stored in a database table). It only ensures that the cache does not return outdated data.
835
     *
836
     * This function can be used to help implement localisable caches (where the cache could be
837
     * stored on a local server as well as a shared cache). The version will be recorded alongside
838
     * the item and get_versioned will always return the correct version.
839
     *
840
     * The version number must be an integer that always increases. This could be based on the
841
     * current time, or a stored value that increases by 1 each time it changes, etc.
842
     *
843
     * If you use this function you must use get_versioned to retrieve the data.
844
     *
845
     * @param string|int $key The key for the data being set.
846
     * @param int $version Integer for the version of the data
847
     * @param mixed $data The data to set against the key.
848
     * @return bool True on success, false otherwise.
849
     */
850
    public function set_versioned($key, int $version, $data): bool {
851
        return $this->set_implementation($key, $version, $data);
852
    }
853
 
854
    /**
855
     * Sets the value for the given key, optionally with a version tag.
856
     *
857
     * @param string|int $key The key for the data being set.
858
     * @param int $version Version number for the data or cache::VERSION_NONE if none
859
     * @param mixed $data The data to set against the key.
860
     * @param bool $setparents If true, sets all parent loaders, otherwise only this one
861
     * @return bool True on success, false otherwise.
862
     */
863
    protected function set_implementation($key, int $version, $data, bool $setparents = true): bool {
864
        if ($this->loader !== false && $setparents) {
865
            // We have a loader available set it there as well.
866
            // We have to let the loader do its own parsing of data as it may be unique.
867
            if ($version === self::VERSION_NONE) {
868
                $this->loader->set($key, $data);
869
            } else {
870
                $this->loader->set_versioned($key, $version, $data);
871
            }
872
        }
873
        $usestaticacceleration = $this->use_static_acceleration();
874
 
875
        if (is_object($data) && $data instanceof cacheable_object) {
876
            $data = new cache_cached_object($data);
877
        } else if (!$this->store->supports_dereferencing_objects() && !is_scalar($data)) {
878
            // If data is an object it will be a reference.
879
            // If data is an array if may contain references.
880
            // We want to break references so that the cache cannot be modified outside of itself.
881
            // Call the function to unreference it (in the best way possible).
882
            $data = $this->unref($data);
883
        }
884
 
885
        if ($usestaticacceleration) {
886
            // Static acceleration cache should include the cache version wrapper, but not TTL.
887
            if ($version === self::VERSION_NONE) {
888
                $this->static_acceleration_set($key, $data);
889
            } else {
890
                $this->static_acceleration_set($key, new \core_cache\version_wrapper($data, $version));
891
            }
892
        }
893
 
894
        if ($this->has_a_ttl() && !$this->store_supports_native_ttl()) {
895
            $data = new cache_ttl_wrapper($data, $this->definition->get_ttl());
896
        }
897
        $parsedkey = $this->parse_key($key);
898
 
899
        if ($version !== self::VERSION_NONE) {
900
            $data = new \core_cache\version_wrapper($data, $version);
901
        }
902
 
903
        $success = $this->store->set($parsedkey, $data);
904
        if ($this->perfdebug) {
905
            cache_helper::record_cache_set($this->store, $this->definition, 1,
906
                    $this->store->get_last_io_bytes());
907
        }
908
        return $success;
909
    }
910
 
911
    /**
912
     * Removes references where required.
913
     *
914
     * @param stdClass|array $data
915
     * @return mixed What ever was put in but without any references.
916
     */
917
    protected function unref($data) {
918
        if ($this->definition->uses_simple_data()) {
919
            return $data;
920
        }
921
        // Check if it requires serialisation in order to produce a reference free copy.
922
        if ($this->requires_serialisation($data)) {
923
            // Damn, its going to have to be serialise.
924
            $data = serialize($data);
925
            // We unserialise immediately so that we don't have to do it every time on get.
926
            $data = unserialize($data);
927
        } else if (!is_scalar($data)) {
928
            // Its safe to clone, lets do it, its going to beat the pants of serialisation.
929
            $data = $this->deep_clone($data);
930
        }
931
        return $data;
932
    }
933
 
934
    /**
935
     * Checks to see if a var requires serialisation.
936
     *
937
     * @param mixed $value The value to check.
938
     * @param int $depth Used to ensure we don't enter an endless loop (think recursion).
939
     * @return bool Returns true if the value is going to require serialisation in order to ensure a reference free copy
940
     *      or false if its safe to clone.
941
     */
942
    protected function requires_serialisation($value, $depth = 1) {
943
        if (is_scalar($value)) {
944
            return false;
945
        } else if (is_array($value) || $value instanceof stdClass || $value instanceof Traversable) {
946
            if ($depth > 5) {
947
                // Skrew it, mega-deep object, developer you suck, we're just going to serialise.
948
                return true;
949
            }
950
            foreach ($value as $key => $subvalue) {
951
                if ($this->requires_serialisation($subvalue, $depth++)) {
952
                    return true;
953
                }
954
            }
955
        }
956
        // Its not scalar, array, or stdClass so we'll need to serialise.
957
        return true;
958
    }
959
 
960
    /**
961
     * Creates a reference free clone of the given value.
962
     *
963
     * @param mixed $value
964
     * @return mixed
965
     */
966
    protected function deep_clone($value) {
967
        if (is_object($value)) {
968
            // Objects must be cloned to begin with.
969
            $value = clone $value;
970
        }
971
        if (is_array($value) || is_object($value)) {
972
            foreach ($value as $key => $subvalue) {
973
                $value[$key] = $this->deep_clone($subvalue);
974
            }
975
        }
976
        return $value;
977
    }
978
 
979
    /**
980
     * Sends several key => value pairs to the cache.
981
     *
982
     * Using this function comes with potential performance implications.
983
     * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call
984
     * the equivalent singular method for each item provided.
985
     * This should not deter you from using this function as there is a performance benefit in situations where the cache store
986
     * does support it, but you should be aware of this fact.
987
     *
988
     * <code>
989
     * // This code will add four entries to the cache, one for each url.
990
     * $cache->set_many(array(
991
     *     'main' => 'http://moodle.org',
992
     *     'docs' => 'http://docs.moodle.org',
993
     *     'tracker' => 'http://tracker.moodle.org',
994
     *     'qa' => ''http://qa.moodle.net'
995
     * ));
996
     * </code>
997
     *
998
     * @param array $keyvaluearray An array of key => value pairs to send to the cache.
999
     * @return int The number of items successfully set. It is up to the developer to check this matches the number of items.
1000
     *      ... if they care that is.
1001
     */
1002
    public function set_many(array $keyvaluearray) {
1003
        if ($this->loader !== false) {
1004
            // We have a loader available set it there as well.
1005
            // We have to let the loader do its own parsing of data as it may be unique.
1006
            $this->loader->set_many($keyvaluearray);
1007
        }
1008
        $data = array();
1009
        $simulatettl = $this->has_a_ttl() && !$this->store_supports_native_ttl();
1010
        $usestaticaccelerationarray = $this->use_static_acceleration();
1011
        $needsdereferencing = !$this->store->supports_dereferencing_objects();
1012
        foreach ($keyvaluearray as $key => $value) {
1013
            if (is_object($value) && $value instanceof cacheable_object) {
1014
                $value = new cache_cached_object($value);
1015
            } else if ($needsdereferencing && !is_scalar($value)) {
1016
                // If data is an object it will be a reference.
1017
                // If data is an array if may contain references.
1018
                // We want to break references so that the cache cannot be modified outside of itself.
1019
                // Call the function to unreference it (in the best way possible).
1020
                $value = $this->unref($value);
1021
            }
1022
            if ($usestaticaccelerationarray) {
1023
                $this->static_acceleration_set($key, $value);
1024
            }
1025
            if ($simulatettl) {
1026
                $value = new cache_ttl_wrapper($value, $this->definition->get_ttl());
1027
            }
1028
            $data[$key] = array(
1029
                'key' => $this->parse_key($key),
1030
                'value' => $value
1031
            );
1032
        }
1033
        $successfullyset = $this->store->set_many($data);
1034
        if ($this->perfdebug && $successfullyset) {
1035
            cache_helper::record_cache_set($this->store, $this->definition, $successfullyset,
1036
                    $this->store->get_last_io_bytes());
1037
        }
1038
        return $successfullyset;
1039
    }
1040
 
1041
    /**
1042
     * Test is a cache has a key.
1043
     *
1044
     * The use of the has methods is strongly discouraged. In a high load environment the cache may well change between the
1045
     * test and any subsequent action (get, set, delete etc).
1046
     * Instead it is recommended to write your code in such a way they it performs the following steps:
1047
     * <ol>
1048
     * <li>Attempt to retrieve the information.</li>
1049
     * <li>Generate the information.</li>
1050
     * <li>Attempt to set the information</li>
1051
     * </ol>
1052
     *
1053
     * Its also worth mentioning that not all stores support key tests.
1054
     * For stores that don't support key tests this functionality is mimicked by using the equivalent get method.
1055
     * Just one more reason you should not use these methods unless you have a very good reason to do so.
1056
     *
1057
     * @param string|int $key
1058
     * @param bool $tryloadifpossible If set to true, the cache doesn't contain the key, and there is another cache loader or
1059
     *      data source then the code will try load the key value from the next item in the chain.
1060
     * @return bool True if the cache has the requested key, false otherwise.
1061
     */
1062
    public function has($key, $tryloadifpossible = false) {
1063
        if ($this->static_acceleration_has($key)) {
1064
            // Hoorah, that was easy. It exists in the static acceleration array so we definitely have it.
1065
            return true;
1066
        }
1067
        $parsedkey = $this->parse_key($key);
1068
 
1069
        if ($this->has_a_ttl() && !$this->store_supports_native_ttl()) {
1070
            // The data has a TTL and the store doesn't support it natively.
1071
            // We must fetch the data and expect a ttl wrapper.
1072
            $data = $this->store->get($parsedkey);
1073
            $has = ($data instanceof cache_ttl_wrapper && !$data->has_expired());
1074
        } else if (!$this->store_supports_key_awareness()) {
1075
            // The store doesn't support key awareness, get the data and check it manually... puke.
1076
            // Either no TTL is set of the store supports its handling natively.
1077
            $data = $this->store->get($parsedkey);
1078
            $has = ($data !== false);
1079
        } else {
1080
            // The store supports key awareness, this is easy!
1081
            // Either no TTL is set of the store supports its handling natively.
1082
            $has = $this->store->has($parsedkey);
1083
        }
1084
        if (!$has && $tryloadifpossible) {
1085
            if ($this->loader !== false) {
1086
                $result = $this->loader->get($parsedkey);
1087
            } else if ($this->datasource !== null) {
1088
                $result = $this->datasource->load_for_cache($key);
1089
            }
1090
            $has = ($result !== null);
1091
            if ($has) {
1092
                $this->set($key, $result);
1093
            }
1094
        }
1095
        return $has;
1096
    }
1097
 
1098
    /**
1099
     * Test is a cache has all of the given keys.
1100
     *
1101
     * It is strongly recommended to avoid the use of this function if not absolutely required.
1102
     * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc).
1103
     *
1104
     * Its also worth mentioning that not all stores support key tests.
1105
     * For stores that don't support key tests this functionality is mimicked by using the equivalent get method.
1106
     * Just one more reason you should not use these methods unless you have a very good reason to do so.
1107
     *
1108
     * @param array $keys
1109
     * @return bool True if the cache has all of the given keys, false otherwise.
1110
     */
1111
    public function has_all(array $keys) {
1112
        if (($this->has_a_ttl() && !$this->store_supports_native_ttl()) || !$this->store_supports_key_awareness()) {
1113
            foreach ($keys as $key) {
1114
                if (!$this->has($key)) {
1115
                    return false;
1116
                }
1117
            }
1118
            return true;
1119
        }
1120
        $parsedkeys = array_map(array($this, 'parse_key'), $keys);
1121
        return $this->store->has_all($parsedkeys);
1122
    }
1123
 
1124
    /**
1125
     * Test if a cache has at least one of the given keys.
1126
     *
1127
     * It is strongly recommended to avoid the use of this function if not absolutely required.
1128
     * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc).
1129
     *
1130
     * Its also worth mentioning that not all stores support key tests.
1131
     * For stores that don't support key tests this functionality is mimicked by using the equivalent get method.
1132
     * Just one more reason you should not use these methods unless you have a very good reason to do so.
1133
     *
1134
     * @param array $keys
1135
     * @return bool True if the cache has at least one of the given keys
1136
     */
1137
    public function has_any(array $keys) {
1138
        if (($this->has_a_ttl() && !$this->store_supports_native_ttl()) || !$this->store_supports_key_awareness()) {
1139
            foreach ($keys as $key) {
1140
                if ($this->has($key)) {
1141
                    return true;
1142
                }
1143
            }
1144
            return false;
1145
        }
1146
 
1147
        if ($this->use_static_acceleration()) {
1148
            foreach ($keys as $id => $key) {
1149
                if ($this->static_acceleration_has($key)) {
1150
                    return true;
1151
                }
1152
            }
1153
        }
1154
        $parsedkeys = array_map(array($this, 'parse_key'), $keys);
1155
        return $this->store->has_any($parsedkeys);
1156
    }
1157
 
1158
    /**
1159
     * Delete the given key from the cache.
1160
     *
1161
     * @param string|int $key The key to delete.
1162
     * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores.
1163
     *     This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this.
1164
     * @return bool True of success, false otherwise.
1165
     */
1166
    public function delete($key, $recurse = true) {
1167
        $this->static_acceleration_delete($key);
1168
        if ($recurse && $this->loader !== false) {
1169
            // Delete from the bottom of the stack first.
1170
            $this->loader->delete($key, $recurse);
1171
        }
1172
        $parsedkey = $this->parse_key($key);
1173
        return $this->store->delete($parsedkey);
1174
    }
1175
 
1176
    /**
1177
     * Delete all of the given keys from the cache.
1178
     *
1179
     * @param array $keys The key to delete.
1180
     * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores.
1181
     *     This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this.
1182
     * @return int The number of items successfully deleted.
1183
     */
1184
    public function delete_many(array $keys, $recurse = true) {
1185
        if ($this->use_static_acceleration()) {
1186
            foreach ($keys as $key) {
1187
                $this->static_acceleration_delete($key);
1188
            }
1189
        }
1190
        if ($recurse && $this->loader !== false) {
1191
            // Delete from the bottom of the stack first.
1192
            $this->loader->delete_many($keys, $recurse);
1193
        }
1194
        $parsedkeys = array_map(array($this, 'parse_key'), $keys);
1195
        return $this->store->delete_many($parsedkeys);
1196
    }
1197
 
1198
    /**
1199
     * Purges the cache store, and loader if there is one.
1200
     *
1201
     * @return bool True on success, false otherwise
1202
     */
1203
    public function purge() {
1204
        // 1. Purge the static acceleration array.
1205
        $this->static_acceleration_purge();
1206
        // 2. Purge the store.
1207
        $this->store->purge();
1208
        // 3. Optionally pruge any stacked loaders.
1209
        if ($this->loader) {
1210
            $this->loader->purge();
1211
        }
1212
        return true;
1213
    }
1214
 
1215
    /**
1216
     * Parses the key turning it into a string (or array is required) suitable to be passed to the cache store.
1217
     *
1218
     * @param string|int $key As passed to get|set|delete etc.
1219
     * @return string|array String unless the store supports multi-identifiers in which case an array if returned.
1220
     */
1221
    protected function parse_key($key) {
1222
        // First up if the store supports multiple keys we'll go with that.
1223
        if ($this->store->supports_multiple_identifiers()) {
1224
            $result = $this->definition->generate_multi_key_parts();
1225
            $result['key'] = $key;
1226
            return $result;
1227
        }
1228
        // If not we need to generate a hash and to for that we use the cache_helper.
1229
        return cache_helper::hash_key($key, $this->definition);
1230
    }
1231
 
1232
    /**
1233
     * Returns true if the cache is making use of a ttl.
1234
     * @return bool
1235
     */
1236
    protected function has_a_ttl() {
1237
        return $this->hasattl;
1238
    }
1239
 
1240
    /**
1241
     * Returns true if the cache store supports native ttl.
1242
     * @return bool
1243
     */
1244
    protected function store_supports_native_ttl() {
1245
        if ($this->supportsnativettl === null) {
1246
            $this->supportsnativettl = ($this->store->supports_native_ttl());
1247
        }
1248
        return $this->supportsnativettl;
1249
    }
1250
 
1251
    /**
1252
     * Returns the cache definition.
1253
     *
1254
     * @return cache_definition
1255
     */
1256
    protected function get_definition() {
1257
        return $this->definition;
1258
    }
1259
 
1260
    /**
1261
     * Returns the cache store
1262
     *
1263
     * @return cache_store
1264
     */
1265
    protected function get_store() {
1266
        return $this->store;
1267
    }
1268
 
1269
    /**
1270
     * Returns the loader associated with this instance.
1271
     *
1272
     * @since Moodle 2.4.4
1273
     * @return cache|false
1274
     */
1275
    protected function get_loader() {
1276
        return $this->loader;
1277
    }
1278
 
1279
    /**
1280
     * Returns the data source associated with this cache.
1281
     *
1282
     * @since Moodle 2.4.4
1283
     * @return cache_data_source|false
1284
     */
1285
    protected function get_datasource() {
1286
        return $this->datasource;
1287
    }
1288
 
1289
    /**
1290
     * Returns true if the store supports key awareness.
1291
     *
1292
     * @return bool
1293
     */
1294
    protected function store_supports_key_awareness() {
1295
        if ($this->supportskeyawareness === null) {
1296
            $this->supportskeyawareness = ($this->store instanceof cache_is_key_aware);
1297
        }
1298
        return $this->supportskeyawareness;
1299
    }
1300
 
1301
    /**
1302
     * Returns true if the store natively supports locking.
1303
     *
1304
     * @return bool
1305
     */
1306
    protected function store_supports_native_locking() {
1307
        if ($this->nativelocking === null) {
1308
            $this->nativelocking = ($this->store instanceof cache_is_lockable);
1309
        }
1310
        return $this->nativelocking;
1311
    }
1312
 
1313
    /**
1314
     * @deprecated since 2.6
1315
     * @see cache::use_static_acceleration()
1316
     */
1317
    protected function is_using_persist_cache() {
1318
        throw new coding_exception('cache::is_using_persist_cache() can not be used anymore.' .
1319
            ' Please use cache::use_static_acceleration() instead.');
1320
    }
1321
 
1322
    /**
1323
     * Returns true if this cache is making use of the static acceleration array.
1324
     *
1325
     * @return bool
1326
     */
1327
    protected function use_static_acceleration() {
1328
        return $this->staticacceleration;
1329
    }
1330
 
1331
    /**
1332
     * @see cache::static_acceleration_has
1333
     * @deprecated since 2.6
1334
     */
1335
    protected function is_in_persist_cache() {
1336
        throw new coding_exception('cache::is_in_persist_cache() can not be used anymore.' .
1337
            ' Please use cache::static_acceleration_has() instead.');
1338
    }
1339
 
1340
    /**
1341
     * Returns true if the requested key exists within the static acceleration array.
1342
     *
1343
     * @param string $key The parsed key
1344
     * @return bool
1345
     */
1346
    protected function static_acceleration_has($key) {
1347
        // This could be written as a single line, however it has been split because the ttl check is faster than the instanceof
1348
        // and has_expired calls.
1349
        if (!$this->staticacceleration || !isset($this->staticaccelerationarray[$key])) {
1350
            return false;
1351
        }
1352
        return true;
1353
    }
1354
 
1355
    /**
1356
     * @deprecated since 2.6
1357
     * @see cache::static_acceleration_get
1358
     */
1359
    protected function get_from_persist_cache() {
1360
        throw new coding_exception('cache::get_from_persist_cache() can not be used anymore.' .
1361
            ' Please use cache::static_acceleration_get() instead.');
1362
    }
1363
 
1364
    /**
1365
     * Returns the item from the static acceleration array if it exists there.
1366
     *
1367
     * @param string $key The parsed key
1368
     * @return mixed|false Dereferenced data from the static acceleration array or false if it wasn't there.
1369
     */
1370
    protected function static_acceleration_get($key) {
1371
        if (!$this->staticacceleration || !isset($this->staticaccelerationarray[$key])) {
1372
            $result = false;
1373
        } else {
1374
            $data = $this->staticaccelerationarray[$key]['data'];
1375
 
1376
            if ($data instanceof cache_cached_object) {
1377
                $result = $data->restore_object();
1378
            } else if ($this->staticaccelerationarray[$key]['serialized']) {
1379
                $result = unserialize($data);
1380
            } else {
1381
                $result = $data;
1382
            }
1383
        }
1384
        if (cache_helper::result_found($result)) {
1385
            if ($this->perfdebug) {
1386
                cache_helper::record_cache_hit(cache_store::STATIC_ACCEL, $this->definition);
1387
            }
1388
            if ($this->staticaccelerationsize > 1 && $this->staticaccelerationcount > 1) {
1389
                // Check to see if this is the last item on the static acceleration keys array.
1390
                if (end($this->staticaccelerationkeys) !== $key) {
1391
                    // It isn't the last item.
1392
                    // Move the item to the end of the array so that it is last to be removed.
1393
                    unset($this->staticaccelerationkeys[$key]);
1394
                    $this->staticaccelerationkeys[$key] = $key;
1395
                }
1396
            }
1397
            return $result;
1398
        } else {
1399
            if ($this->perfdebug) {
1400
                cache_helper::record_cache_miss(cache_store::STATIC_ACCEL, $this->definition);
1401
            }
1402
            return false;
1403
        }
1404
    }
1405
 
1406
    /**
1407
     * @deprecated since 2.6
1408
     * @see cache::static_acceleration_set
1409
     */
1410
    protected function set_in_persist_cache() {
1411
        throw new coding_exception('cache::set_in_persist_cache() can not be used anymore.' .
1412
            ' Please use cache::static_acceleration_set() instead.');
1413
    }
1414
 
1415
    /**
1416
     * Sets a key value pair into the static acceleration array.
1417
     *
1418
     * @param string $key The parsed key
1419
     * @param mixed $data
1420
     * @return bool
1421
     */
1422
    protected function static_acceleration_set($key, $data) {
1423
        if ($this->staticaccelerationsize !== false && isset($this->staticaccelerationkeys[$key])) {
1424
            $this->staticaccelerationcount--;
1425
            unset($this->staticaccelerationkeys[$key]);
1426
        }
1427
 
1428
        // We serialize anything that's not;
1429
        // 1. A known scalar safe value.
1430
        // 2. A definition that says it's simpledata.  We trust it that it doesn't contain dangerous references.
1431
        // 3. An object that handles dereferencing by itself.
1432
        if (is_scalar($data) || $this->definition->uses_simple_data()
1433
                || $data instanceof cache_cached_object) {
1434
            $this->staticaccelerationarray[$key]['data'] = $data;
1435
            $this->staticaccelerationarray[$key]['serialized'] = false;
1436
        } else {
1437
            $this->staticaccelerationarray[$key]['data'] = serialize($data);
1438
            $this->staticaccelerationarray[$key]['serialized'] = true;
1439
        }
1440
        if ($this->staticaccelerationsize !== false) {
1441
            $this->staticaccelerationcount++;
1442
            $this->staticaccelerationkeys[$key] = $key;
1443
            if ($this->staticaccelerationcount > $this->staticaccelerationsize) {
1444
                $dropkey = array_shift($this->staticaccelerationkeys);
1445
                unset($this->staticaccelerationarray[$dropkey]);
1446
                $this->staticaccelerationcount--;
1447
            }
1448
        }
1449
        return true;
1450
    }
1451
 
1452
    /**
1453
     * @deprecated since 2.6
1454
     * @see cache::static_acceleration_delete()
1455
     */
1456
    protected function delete_from_persist_cache() {
1457
        throw new coding_exception('cache::delete_from_persist_cache() can not be used anymore.' .
1458
            ' Please use cache::static_acceleration_delete() instead.');
1459
    }
1460
 
1461
    /**
1462
     * Deletes an item from the static acceleration array.
1463
     *
1464
     * @param string|int $key As given to get|set|delete
1465
     * @return bool True on success, false otherwise.
1466
     */
1467
    protected function static_acceleration_delete($key) {
1468
        unset($this->staticaccelerationarray[$key]);
1469
        if ($this->staticaccelerationsize !== false && isset($this->staticaccelerationkeys[$key])) {
1470
            unset($this->staticaccelerationkeys[$key]);
1471
            $this->staticaccelerationcount--;
1472
        }
1473
        return true;
1474
    }
1475
 
1476
    /**
1477
     * Purge the static acceleration cache.
1478
     */
1479
    protected function static_acceleration_purge() {
1480
        $this->staticaccelerationarray = array();
1481
        if ($this->staticaccelerationsize !== false) {
1482
            $this->staticaccelerationkeys = array();
1483
            $this->staticaccelerationcount = 0;
1484
        }
1485
    }
1486
 
1487
    /**
1488
     * Returns the timestamp from the first request for the time from the cache API.
1489
     *
1490
     * This stamp needs to be used for all ttl and time based operations to ensure that we don't end up with
1491
     * timing issues.
1492
     *
1493
     * @param   bool    $float Whether to use floating precision accuracy.
1494
     * @return  int|float
1495
     */
1496
    public static function now($float = false) {
1497
        if (self::$now === null) {
1498
            self::$now = microtime(true);
1499
        }
1500
 
1501
        if ($float) {
1502
            return self::$now;
1503
        } else {
1504
            return (int) self::$now;
1505
        }
1506
    }
1507
 
1508
    /**
1509
     * Get a 'purge' token used for cache invalidation handling.
1510
     *
1511
     * Note: This function is intended for use from within the Cache API only and not by plugins, or cache stores.
1512
     *
1513
     * @param   bool    $reset  Whether to reset the token and generate a new one.
1514
     * @return  string
1515
     */
1516
    public static function get_purge_token($reset = false) {
1517
        if (self::$purgetoken === null || $reset) {
1518
            self::$now = null;
1519
            self::$purgetoken = self::now(true) . '-' . uniqid('', true);
1520
        }
1521
 
1522
        return self::$purgetoken;
1523
    }
1524
 
1525
    /**
1526
     * Compare a pair of purge tokens.
1527
     *
1528
     * If the two tokens are identical, then the return value is 0.
1529
     * If the time component of token A is newer than token B, then a positive value is returned.
1530
     * If the time component of token B is newer than token A, then a negative value is returned.
1531
     *
1532
     * Note: This function is intended for use from within the Cache API only and not by plugins, or cache stores.
1533
     *
1534
     * @param   string  $tokena
1535
     * @param   string  $tokenb
1536
     * @return  int
1537
     */
1538
    public static function compare_purge_tokens($tokena, $tokenb) {
1539
        if ($tokena === $tokenb) {
1540
            // There is an exact match.
1541
            return 0;
1542
        }
1543
 
1544
        // The token for when the cache was last invalidated.
1545
        list($atime) = explode('-', "{$tokena}-", 2);
1546
 
1547
        // The token for this cache.
1548
        list($btime) = explode('-', "{$tokenb}-", 2);
1549
 
1550
        if ($atime >= $btime) {
1551
            // Token A is newer.
1552
            return 1;
1553
        } else {
1554
            // Token A is older.
1555
            return -1;
1556
        }
1557
    }
1558
 
1559
    /**
1560
     * Subclasses may support purging cache of all data belonging to the
1561
     * current user.
1562
     */
1563
    public function purge_current_user() {
1564
    }
1565
}
1566
 
1567
/**
1568
 * An application cache.
1569
 *
1570
 * This class is used for application caches returned by the cache::make methods.
1571
 * On top of the standard functionality it also allows locking to be required and or manually operated.
1572
 *
1573
 * This cache class should never be interacted with directly. Instead you should always use the cache::make methods.
1574
 * It is technically possible to call those methods through this class however there is no guarantee that you will get an
1575
 * instance of this class back again.
1576
 *
1577
 * @internal don't use me directly.
1578
 *
1579
 * @package    core
1580
 * @category   cache
1581
 * @copyright  2012 Sam Hemelryk
1582
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1583
 */
1584
class cache_application extends cache implements cache_loader_with_locking {
1585
 
1586
    /**
1587
     * Lock identifier.
1588
     * This is used to ensure the lock belongs to the cache instance + definition + user.
1589
     * @var string
1590
     */
1591
    protected $lockidentifier;
1592
 
1593
    /**
1594
     * Gets set to true if the cache's primary store natively supports locking.
1595
     * If it does then we use that, otherwise we need to instantiate a second store to use for locking.
1596
     * @var cache_store
1597
     */
1598
    protected $nativelocking = null;
1599
 
1600
    /**
1601
     * Gets set to true if the cache is going to be using locking.
1602
     * This isn't a requirement, it doesn't need to use locking (most won't) and this bool is used to quickly check things.
1603
     * If required then locking will be forced for the get|set|delete operation.
1604
     * @var bool
1605
     */
1606
    protected $requirelocking = false;
1607
 
1608
    /**
1609
     * Gets set to true if the cache writes (set|delete) must have a manual lock created first
1610
     * @var bool
1611
     */
1612
    protected $requirelockingbeforewrite = false;
1613
 
1614
    /**
1615
     * Gets set to a cache_store to use for locking if the caches primary store doesn't support locking natively.
1616
     * @var cache_lock_interface
1617
     */
1618
    protected $cachelockinstance;
1619
 
1620
    /**
1621
     * Store a list of locks acquired by this process.
1622
     * @var array
1623
     */
1624
    protected $locks;
1625
 
1626
    /**
1627
     * Overrides the cache construct method.
1628
     *
1629
     * You should not call this method from your code, instead you should use the cache::make methods.
1630
     *
1631
     * @param cache_definition $definition
1632
     * @param cache_store $store
1633
     * @param cache_loader|cache_data_source $loader
1634
     */
1635
    public function __construct(cache_definition $definition, cache_store $store, $loader = null) {
1636
        parent::__construct($definition, $store, $loader);
1637
        $this->nativelocking = $this->store_supports_native_locking();
1638
        if ($definition->require_locking()) {
1639
            $this->requirelocking = true;
1640
            $this->requirelockingbeforewrite = $definition->require_locking_before_write();
1641
        }
1642
 
1643
        $this->handle_invalidation_events();
1644
    }
1645
 
1646
    /**
1647
     * Returns the identifier to use
1648
     *
1649
     * @staticvar int $instances Counts the number of instances. Used as part of the lock identifier.
1650
     * @return string
1651
     */
1652
    public function get_identifier() {
1653
        static $instances = 0;
1654
        if ($this->lockidentifier === null) {
1655
            $this->lockidentifier = md5(
1656
                $this->get_definition()->generate_definition_hash() .
1657
                sesskey() .
1658
                $instances++ .
1659
                'cache_application'
1660
            );
1661
        }
1662
        return $this->lockidentifier;
1663
    }
1664
 
1665
    /**
1666
     * Fixes the instance up after a clone.
1667
     */
1668
    public function __clone() {
1669
        // Force a new idenfitier.
1670
        $this->lockidentifier = null;
1671
    }
1672
 
1673
    /**
1674
     * Acquires a lock on the given key.
1675
     *
1676
     * This is done automatically if the definition requires it.
1677
     * It is recommended to use a definition if you want to have locking although it is possible to do locking without having
1678
     * it required by the definition.
1679
     * The problem with such an approach is that you cannot ensure that code will consistently use locking. You will need to
1680
     * rely on the integrators review skills.
1681
     *
1682
     * @param string|int $key The key as given to get|set|delete
1683
     * @return bool Always returns true
1684
     * @throws moodle_exception If the lock cannot be obtained
1685
     */
1686
    public function acquire_lock($key) {
1687
        $releaseparent = false;
1688
        try {
1689
            if ($this->get_loader() !== false) {
1690
                $this->get_loader()->acquire_lock($key);
1691
                // We need to release this lock later if the lock is not successful.
1692
                $releaseparent = true;
1693
            }
1694
            $hashedkey = cache_helper::hash_key($key, $this->get_definition());
1695
            $before = microtime(true);
1696
            if ($this->nativelocking) {
1697
                $lock = $this->get_store()->acquire_lock($hashedkey, $this->get_identifier());
1698
            } else {
1699
                $this->ensure_cachelock_available();
1700
                $lock = $this->cachelockinstance->lock($hashedkey, $this->get_identifier());
1701
            }
1702
            $after = microtime(true);
1703
            if ($lock) {
1704
                $this->locks[$hashedkey] = $lock;
1705
                if (MDL_PERF || $this->perfdebug) {
1706
                    \core\lock\timing_wrapper_lock_factory::record_lock_data($after, $before,
1707
                        $this->get_definition()->get_id(), $hashedkey, $lock, $this->get_identifier() . $hashedkey);
1708
                }
1709
                $releaseparent = false;
1710
                return true;
1711
            } else {
1712
                throw new moodle_exception('ex_unabletolock', 'cache', '', null,
1713
                    'store: ' . get_class($this->get_store()) . ', lock: ' . $hashedkey);
1714
            }
1715
        } finally {
1716
            // Release the parent lock if we acquired it, then threw an exception.
1717
            if ($releaseparent) {
1718
                $this->get_loader()->release_lock($key);
1719
            }
1720
        }
1721
    }
1722
 
1723
    /**
1724
     * Checks if this cache has a lock on the given key.
1725
     *
1726
     * @param string|int $key The key as given to get|set|delete
1727
     * @return bool|null Returns true if there is a lock and this cache has it, null if no one has a lock on that key, false if
1728
     *      someone else has the lock.
1729
     */
1730
    public function check_lock_state($key) {
1731
        $key = cache_helper::hash_key($key, $this->get_definition());
1732
        if (!empty($this->locks[$key])) {
1733
            return true; // Shortcut to save having to make a call to the cache store if the lock is held by this process.
1734
        }
1735
        if ($this->nativelocking) {
1736
            return $this->get_store()->check_lock_state($key, $this->get_identifier());
1737
        } else {
1738
            $this->ensure_cachelock_available();
1739
            return $this->cachelockinstance->check_state($key, $this->get_identifier());
1740
        }
1741
    }
1742
 
1743
    /**
1744
     * Releases the lock this cache has on the given key
1745
     *
1746
     * @param string|int $key
1747
     * @return bool True if the operation succeeded, false otherwise.
1748
     */
1749
    public function release_lock($key) {
1750
        $loaderkey = $key;
1751
        $key = cache_helper::hash_key($key, $this->get_definition());
1752
        if ($this->nativelocking) {
1753
            $released = $this->get_store()->release_lock($key, $this->get_identifier());
1754
        } else {
1755
            $this->ensure_cachelock_available();
1756
            $released = $this->cachelockinstance->unlock($key, $this->get_identifier());
1757
        }
1758
        if ($released && array_key_exists($key, $this->locks)) {
1759
            unset($this->locks[$key]);
1760
            if (MDL_PERF || $this->perfdebug) {
1761
                \core\lock\timing_wrapper_lock_factory::record_lock_released_data($this->get_identifier() . $key);
1762
            }
1763
        }
1764
        if ($this->get_loader() !== false) {
1765
            $this->get_loader()->release_lock($loaderkey);
1766
        }
1767
        return $released;
1768
    }
1769
 
1770
    /**
1771
     * Ensure that the dedicated lock store is ready to go.
1772
     *
1773
     * This should only happen if the cache store doesn't natively support it.
1774
     */
1775
    protected function ensure_cachelock_available() {
1776
        if ($this->cachelockinstance === null) {
1777
            $this->cachelockinstance = cache_helper::get_cachelock_for_store($this->get_store());
1778
        }
1779
    }
1780
 
1781
    /**
1782
     * Sends a key => value pair to the cache.
1783
     *
1784
     * <code>
1785
     * // This code will add four entries to the cache, one for each url.
1786
     * $cache->set('main', 'http://moodle.org');
1787
     * $cache->set('docs', 'http://docs.moodle.org');
1788
     * $cache->set('tracker', 'http://tracker.moodle.org');
1789
     * $cache->set('qa', 'http://qa.moodle.net');
1790
     * </code>
1791
     *
1792
     * @param string|int $key The key for the data being requested.
1793
     * @param int $version Version number
1794
     * @param mixed $data The data to set against the key.
1795
     * @param bool $setparents If true, sets all parent loaders, otherwise only this one
1796
     * @return bool True on success, false otherwise.
1797
     * @throws coding_exception If a required lock has not beeen acquired
1798
     */
1799
    protected function set_implementation($key, int $version, $data, bool $setparents = true): bool {
1800
        if ($this->requirelockingbeforewrite && !$this->check_lock_state($key)) {
1801
            throw new coding_exception('Attempted to set cache key "' . $key . '" without a lock. '
1802
                . 'Locking before writes is required for ' . $this->get_definition()->get_id());
1803
        }
1804
        return parent::set_implementation($key, $version, $data, $setparents);
1805
    }
1806
 
1807
    /**
1808
     * Sends several key => value pairs to the cache.
1809
     *
1810
     * Using this function comes with potential performance implications.
1811
     * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call
1812
     * the equivalent singular method for each item provided.
1813
     * This should not deter you from using this function as there is a performance benefit in situations where the cache store
1814
     * does support it, but you should be aware of this fact.
1815
     *
1816
     * <code>
1817
     * // This code will add four entries to the cache, one for each url.
1818
     * $cache->set_many(array(
1819
     *     'main' => 'http://moodle.org',
1820
     *     'docs' => 'http://docs.moodle.org',
1821
     *     'tracker' => 'http://tracker.moodle.org',
1822
     *     'qa' => ''http://qa.moodle.net'
1823
     * ));
1824
     * </code>
1825
     *
1826
     * @param array $keyvaluearray An array of key => value pairs to send to the cache.
1827
     * @return int The number of items successfully set. It is up to the developer to check this matches the number of items.
1828
     *      ... if they care that is.
1829
     * @throws coding_exception If a required lock has not beeen acquired
1830
     */
1831
    public function set_many(array $keyvaluearray) {
1832
        if ($this->requirelockingbeforewrite) {
1833
            foreach ($keyvaluearray as $key => $value) {
1834
                if (!$this->check_lock_state($key)) {
1835
                    throw new coding_exception('Attempted to set cache key "' . $key . '" without a lock. '
1836
                            . 'Locking before writes is required for ' . $this->get_definition()->get_id());
1837
                }
1838
            }
1839
        }
1840
        return parent::set_many($keyvaluearray);
1841
    }
1842
 
1843
    /**
1844
     * Delete the given key from the cache.
1845
     *
1846
     * @param string|int $key The key to delete.
1847
     * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores.
1848
     *     This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this.
1849
     * @return bool True of success, false otherwise.
1850
     * @throws coding_exception If a required lock has not beeen acquired
1851
     */
1852
    public function delete($key, $recurse = true) {
1853
        if ($this->requirelockingbeforewrite && !$this->check_lock_state($key)) {
1854
            throw new coding_exception('Attempted to delete cache key "' . $key . '" without a lock. '
1855
                    . 'Locking before writes is required for ' . $this->get_definition()->get_id());
1856
        }
1857
        return parent::delete($key, $recurse);
1858
    }
1859
 
1860
    /**
1861
     * Delete all of the given keys from the cache.
1862
     *
1863
     * @param array $keys The key to delete.
1864
     * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores.
1865
     *     This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this.
1866
     * @return int The number of items successfully deleted.
1867
     * @throws coding_exception If a required lock has not beeen acquired
1868
     */
1869
    public function delete_many(array $keys, $recurse = true) {
1870
        if ($this->requirelockingbeforewrite) {
1871
            foreach ($keys as $key) {
1872
                if (!$this->check_lock_state($key)) {
1873
                    throw new coding_exception('Attempted to delete cache key "' . $key . '" without a lock. '
1874
                            . 'Locking before writes is required for ' . $this->get_definition()->get_id());
1875
                }
1876
            }
1877
        }
1878
        return parent::delete_many($keys, $recurse);
1879
    }
1880
}
1881
 
1882
/**
1883
 * A session cache.
1884
 *
1885
 * This class is used for session caches returned by the cache::make methods.
1886
 *
1887
 * It differs from the application loader in a couple of noteable ways:
1888
 *    1. Sessions are always expected to exist.
1889
 *       Because of this we don't ever use the static acceleration array.
1890
 *    2. Session data for a loader instance (store + definition) is consolidate into a
1891
 *       single array for storage within the store.
1892
 *       Along with this we embed a lastaccessed time with the data. This way we can
1893
 *       check sessions for a last access time.
1894
 *    3. Session stores are required to support key searching and must
1895
 *       implement cache_is_searchable. This ensures stores used for the cache can be
1896
 *       targetted for garbage collection of session data.
1897
 *
1898
 * This cache class should never be interacted with directly. Instead you should always use the cache::make methods.
1899
 * It is technically possible to call those methods through this class however there is no guarantee that you will get an
1900
 * instance of this class back again.
1901
 *
1902
 * @todo we should support locking in the session as well. Should be pretty simple to set up.
1903
 *
1904
 * @internal don't use me directly.
1905
 * @method cache_store|cache_is_searchable get_store() Returns the cache store which must implement both cache_is_searchable.
1906
 *
1907
 * @package    core
1908
 * @category   cache
1909
 * @copyright  2012 Sam Hemelryk
1910
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1911
 */
1912
class cache_session extends cache {
1913
    /**
1914
     * The user the session has been established for.
1915
     * @var int
1916
     */
1917
    protected static $loadeduserid = null;
1918
 
1919
    /**
1920
     * The userid this cache is currently using.
1921
     * @var int
1922
     */
1923
    protected $currentuserid = null;
1924
 
1925
    /**
1926
     * The session id we are currently using.
1927
     * @var array
1928
     */
1929
    protected $sessionid = null;
1930
 
1931
    /**
1932
     * The session data for the above session id.
1933
     * @var array
1934
     */
1935
    protected $session = null;
1936
 
1937
    /**
1938
     * Constant used to prefix keys.
1939
     */
1940
    const KEY_PREFIX = 'sess_';
1941
 
1942
    /**
1943
     * This is the key used to track last access.
1944
     */
1945
    const LASTACCESS = '__lastaccess__';
1946
 
1947
    /**
1948
     * Override the cache::construct method.
1949
     *
1950
     * This function gets overriden so that we can process any invalidation events if need be.
1951
     * If the definition doesn't have any invalidation events then this occurs exactly as it would for the cache class.
1952
     * Otherwise we look at the last invalidation time and then check the invalidation data for events that have occured
1953
     * between then now.
1954
     *
1955
     * You should not call this method from your code, instead you should use the cache::make methods.
1956
     *
1957
     * @param cache_definition $definition
1958
     * @param cache_store $store
1959
     * @param cache_loader|cache_data_source $loader
1960
     */
1961
    public function __construct(cache_definition $definition, cache_store $store, $loader = null) {
1962
        // First up copy the loadeduserid to the current user id.
1963
        $this->currentuserid = self::$loadeduserid;
1964
        $this->set_session_id();
1965
        parent::__construct($definition, $store, $loader);
1966
 
1967
        // This will trigger check tracked user. If this gets removed a call to that will need to be added here in its place.
1968
        $this->set(self::LASTACCESS, cache::now());
1969
 
1970
        $this->handle_invalidation_events();
1971
    }
1972
 
1973
    /**
1974
     * Sets the session id for the loader.
1975
     */
1976
    protected function set_session_id() {
1977
        $this->sessionid = preg_replace('#[^a-zA-Z0-9_]#', '_', session_id());
1978
    }
1979
 
1980
    /**
1981
     * Returns the prefix used for all keys.
1982
     * @return string
1983
     */
1984
    protected function get_key_prefix() {
1985
        return 'u'.$this->currentuserid.'_'.$this->sessionid;
1986
    }
1987
 
1988
    /**
1989
     * Parses the key turning it into a string (or array is required) suitable to be passed to the cache store.
1990
     *
1991
     * This function is called for every operation that uses keys. For this reason we use this function to also check
1992
     * that the current user is the same as the user who last used this cache.
1993
     *
1994
     * On top of that if prepends the string 'sess_' to the start of all keys. The _ ensures things are easily identifiable.
1995
     *
1996
     * @param string|int $key As passed to get|set|delete etc.
1997
     * @return string|array String unless the store supports multi-identifiers in which case an array if returned.
1998
     */
1999
    protected function parse_key($key) {
2000
        $prefix = $this->get_key_prefix();
2001
        if ($key === self::LASTACCESS) {
2002
            return $key.$prefix;
2003
        }
2004
        return $prefix.'_'.parent::parse_key($key);
2005
    }
2006
 
2007
    /**
2008
     * Check that this cache instance is tracking the current user.
2009
     */
2010
    protected function check_tracked_user() {
2011
        if (isset($_SESSION['USER']->id) && $_SESSION['USER']->id !== null) {
2012
            // Get the id of the current user.
2013
            $new = $_SESSION['USER']->id;
2014
        } else {
2015
            // No user set up yet.
2016
            $new = 0;
2017
        }
2018
        if ($new !== self::$loadeduserid) {
2019
            // The current user doesn't match the tracked userid for this request.
2020
            if (!is_null(self::$loadeduserid)) {
2021
                // Purge the data we have for the old user.
2022
                // This way we don't bloat the session.
2023
                $this->purge();
2024
            }
2025
            self::$loadeduserid = $new;
2026
            $this->currentuserid = $new;
2027
        } else if ($new !== $this->currentuserid) {
2028
            // The current user matches the loaded user but not the user last used by this cache.
2029
            $this->purge_current_user();
2030
            $this->currentuserid = $new;
2031
        }
2032
    }
2033
 
2034
    /**
2035
     * Purges the session cache of all data belonging to the current user.
2036
     */
2037
    public function purge_current_user() {
2038
        $keys = $this->get_store()->find_by_prefix($this->get_key_prefix());
2039
        $this->get_store()->delete_many($keys);
2040
    }
2041
 
2042
    /**
2043
     * Retrieves the value for the given key from the cache.
2044
     *
2045
     * @param string|int $key The key for the data being requested.
2046
     *      It can be any structure although using a scalar string or int is recommended in the interests of performance.
2047
     *      In advanced cases an array may be useful such as in situations requiring the multi-key functionality.
2048
     * @param int $requiredversion Minimum required version of the data or cache::VERSION_NONE
2049
     * @param int $strictness One of IGNORE_MISSING | MUST_EXIST
2050
     * @param mixed &$actualversion If specified, will be set to the actual version number retrieved
2051
     * @return mixed|false The data from the cache or false if the key did not exist within the cache.
2052
     * @throws coding_exception
2053
     */
2054
    protected function get_implementation($key, int $requiredversion, int $strictness, &$actualversion = null) {
2055
        // Check the tracked user.
2056
        $this->check_tracked_user();
2057
 
2058
        // Use parent code.
2059
        return parent::get_implementation($key, $requiredversion, $strictness, $actualversion);
2060
    }
2061
 
2062
    /**
2063
     * Sends a key => value pair to the cache.
2064
     *
2065
     * <code>
2066
     * // This code will add four entries to the cache, one for each url.
2067
     * $cache->set('main', 'http://moodle.org');
2068
     * $cache->set('docs', 'http://docs.moodle.org');
2069
     * $cache->set('tracker', 'http://tracker.moodle.org');
2070
     * $cache->set('qa', 'http://qa.moodle.net');
2071
     * </code>
2072
     *
2073
     * @param string|int $key The key for the data being requested.
2074
     *      It can be any structure although using a scalar string or int is recommended in the interests of performance.
2075
     *      In advanced cases an array may be useful such as in situations requiring the multi-key functionality.
2076
     * @param mixed $data The data to set against the key.
2077
     * @return bool True on success, false otherwise.
2078
     */
2079
    public function set($key, $data) {
2080
        $this->check_tracked_user();
2081
        $loader = $this->get_loader();
2082
        if ($loader !== false) {
2083
            // We have a loader available set it there as well.
2084
            // We have to let the loader do its own parsing of data as it may be unique.
2085
            $loader->set($key, $data);
2086
        }
2087
        if (is_object($data) && $data instanceof cacheable_object) {
2088
            $data = new cache_cached_object($data);
2089
        } else if (!$this->get_store()->supports_dereferencing_objects() && !is_scalar($data)) {
2090
            // If data is an object it will be a reference.
2091
            // If data is an array if may contain references.
2092
            // We want to break references so that the cache cannot be modified outside of itself.
2093
            // Call the function to unreference it (in the best way possible).
2094
            $data = $this->unref($data);
2095
        }
2096
        // We dont' support native TTL here as we consolidate data for sessions.
2097
        if ($this->has_a_ttl() && !$this->store_supports_native_ttl()) {
2098
            $data = new cache_ttl_wrapper($data, $this->get_definition()->get_ttl());
2099
        }
2100
        $success = $this->get_store()->set($this->parse_key($key), $data);
2101
        if ($this->perfdebug) {
2102
            cache_helper::record_cache_set($this->get_store(), $this->get_definition(), 1,
2103
                    $this->get_store()->get_last_io_bytes());
2104
        }
2105
        return $success;
2106
    }
2107
 
2108
    /**
2109
     * Delete the given key from the cache.
2110
     *
2111
     * @param string|int $key The key to delete.
2112
     * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores.
2113
     *     This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this.
2114
     * @return bool True of success, false otherwise.
2115
     */
2116
    public function delete($key, $recurse = true) {
2117
        $parsedkey = $this->parse_key($key);
2118
        if ($recurse && $this->get_loader() !== false) {
2119
            // Delete from the bottom of the stack first.
2120
            $this->get_loader()->delete($key, $recurse);
2121
        }
2122
        return $this->get_store()->delete($parsedkey);
2123
    }
2124
 
2125
    /**
2126
     * Retrieves an array of values for an array of keys.
2127
     *
2128
     * Using this function comes with potential performance implications.
2129
     * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call
2130
     * the equivalent singular method for each item provided.
2131
     * This should not deter you from using this function as there is a performance benefit in situations where the cache store
2132
     * does support it, but you should be aware of this fact.
2133
     *
2134
     * @param array $keys The keys of the data being requested.
2135
     *      Each key can be any structure although using a scalar string or int is recommended in the interests of performance.
2136
     *      In advanced cases an array may be useful such as in situations requiring the multi-key functionality.
2137
     * @param int $strictness One of IGNORE_MISSING or MUST_EXIST.
2138
     * @return array An array of key value pairs for the items that could be retrieved from the cache.
2139
     *      If MUST_EXIST was used and not all keys existed within the cache then an exception will be thrown.
2140
     *      Otherwise any key that did not exist will have a data value of false within the results.
2141
     * @throws coding_exception
2142
     */
2143
    public function get_many(array $keys, $strictness = IGNORE_MISSING) {
2144
        $this->check_tracked_user();
2145
        $parsedkeys = array();
2146
        $keymap = array();
2147
        foreach ($keys as $key) {
2148
            $parsedkey = $this->parse_key($key);
2149
            $parsedkeys[$key] = $parsedkey;
2150
            $keymap[$parsedkey] = $key;
2151
        }
2152
        $result = $this->get_store()->get_many($parsedkeys);
2153
        if ($this->perfdebug) {
2154
            $readbytes = $this->get_store()->get_last_io_bytes();
2155
        }
2156
        $return = array();
2157
        $missingkeys = array();
2158
        $hasmissingkeys = false;
2159
        foreach ($result as $parsedkey => $value) {
2160
            $key = $keymap[$parsedkey];
2161
            if ($value instanceof cache_ttl_wrapper) {
2162
                /* @var cache_ttl_wrapper $value */
2163
                if ($value->has_expired()) {
2164
                    $this->delete($keymap[$parsedkey]);
2165
                    $value = false;
2166
                } else {
2167
                    $value = $value->data;
2168
                }
2169
            }
2170
            if ($value instanceof cache_cached_object) {
2171
                /* @var cache_cached_object $value */
2172
                $value = $value->restore_object();
2173
            } else if (!$this->get_store()->supports_dereferencing_objects() && !is_scalar($value)) {
2174
                // If data is an object it will be a reference.
2175
                // If data is an array if may contain references.
2176
                // We want to break references so that the cache cannot be modified outside of itself.
2177
                // Call the function to unreference it (in the best way possible).
2178
                $value = $this->unref($value);
2179
            }
2180
            $return[$key] = $value;
2181
            if ($value === false) {
2182
                $hasmissingkeys = true;
2183
                $missingkeys[$parsedkey] = $key;
2184
            }
2185
        }
2186
        if ($hasmissingkeys) {
2187
            // We've got missing keys - we've got to check any loaders or data sources.
2188
            $loader = $this->get_loader();
2189
            $datasource = $this->get_datasource();
2190
            if ($loader !== false) {
2191
                foreach ($loader->get_many($missingkeys) as $key => $value) {
2192
                    if ($value !== false) {
2193
                        $return[$key] = $value;
2194
                        unset($missingkeys[$parsedkeys[$key]]);
2195
                    }
2196
                }
2197
            }
2198
            $hasmissingkeys = count($missingkeys) > 0;
2199
            if ($datasource !== false && $hasmissingkeys) {
2200
                // We're still missing keys but we've got a datasource.
2201
                foreach ($datasource->load_many_for_cache($missingkeys) as $key => $value) {
2202
                    if ($value !== false) {
2203
                        $return[$key] = $value;
2204
                        unset($missingkeys[$parsedkeys[$key]]);
2205
                    }
2206
                }
2207
                $hasmissingkeys = count($missingkeys) > 0;
2208
            }
2209
        }
2210
        if ($hasmissingkeys && $strictness === MUST_EXIST) {
2211
            throw new coding_exception('Requested key did not exist in any cache stores and could not be loaded.');
2212
        }
2213
        if ($this->perfdebug) {
2214
            $hits = 0;
2215
            $misses = 0;
2216
            foreach ($return as $value) {
2217
                if ($value === false) {
2218
                    $misses++;
2219
                } else {
2220
                    $hits++;
2221
                }
2222
            }
2223
            cache_helper::record_cache_hit($this->get_store(), $this->get_definition(), $hits, $readbytes);
2224
            cache_helper::record_cache_miss($this->get_store(), $this->get_definition(), $misses);
2225
        }
2226
        return $return;
2227
 
2228
    }
2229
 
2230
    /**
2231
     * Delete all of the given keys from the cache.
2232
     *
2233
     * @param array $keys The key to delete.
2234
     * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores.
2235
     *     This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this.
2236
     * @return int The number of items successfully deleted.
2237
     */
2238
    public function delete_many(array $keys, $recurse = true) {
2239
        $parsedkeys = array_map(array($this, 'parse_key'), $keys);
2240
        if ($recurse && $this->get_loader() !== false) {
2241
            // Delete from the bottom of the stack first.
2242
            $this->get_loader()->delete_many($keys, $recurse);
2243
        }
2244
        return $this->get_store()->delete_many($parsedkeys);
2245
    }
2246
 
2247
    /**
2248
     * Sends several key => value pairs to the cache.
2249
     *
2250
     * Using this function comes with potential performance implications.
2251
     * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call
2252
     * the equivalent singular method for each item provided.
2253
     * This should not deter you from using this function as there is a performance benefit in situations where the cache store
2254
     * does support it, but you should be aware of this fact.
2255
     *
2256
     * <code>
2257
     * // This code will add four entries to the cache, one for each url.
2258
     * $cache->set_many(array(
2259
     *     'main' => 'http://moodle.org',
2260
     *     'docs' => 'http://docs.moodle.org',
2261
     *     'tracker' => 'http://tracker.moodle.org',
2262
     *     'qa' => ''http://qa.moodle.net'
2263
     * ));
2264
     * </code>
2265
     *
2266
     * @param array $keyvaluearray An array of key => value pairs to send to the cache.
2267
     * @return int The number of items successfully set. It is up to the developer to check this matches the number of items.
2268
     *      ... if they care that is.
2269
     */
2270
    public function set_many(array $keyvaluearray) {
2271
        $this->check_tracked_user();
2272
        $loader = $this->get_loader();
2273
        if ($loader !== false) {
2274
            // We have a loader available set it there as well.
2275
            // We have to let the loader do its own parsing of data as it may be unique.
2276
            $loader->set_many($keyvaluearray);
2277
        }
2278
        $data = array();
2279
        $definitionid = $this->get_definition()->get_ttl();
2280
        $simulatettl = $this->has_a_ttl() && !$this->store_supports_native_ttl();
2281
        foreach ($keyvaluearray as $key => $value) {
2282
            if (is_object($value) && $value instanceof cacheable_object) {
2283
                $value = new cache_cached_object($value);
2284
            } else if (!$this->get_store()->supports_dereferencing_objects() && !is_scalar($value)) {
2285
                // If data is an object it will be a reference.
2286
                // If data is an array if may contain references.
2287
                // We want to break references so that the cache cannot be modified outside of itself.
2288
                // Call the function to unreference it (in the best way possible).
2289
                $value = $this->unref($value);
2290
            }
2291
            if ($simulatettl) {
2292
                $value = new cache_ttl_wrapper($value, $definitionid);
2293
            }
2294
            $data[$key] = array(
2295
                'key' => $this->parse_key($key),
2296
                'value' => $value
2297
            );
2298
        }
2299
        $successfullyset = $this->get_store()->set_many($data);
2300
        if ($this->perfdebug && $successfullyset) {
2301
            cache_helper::record_cache_set($this->get_store(), $this->get_definition(), $successfullyset,
2302
                    $this->get_store()->get_last_io_bytes());
2303
        }
2304
        return $successfullyset;
2305
    }
2306
 
2307
    /**
2308
     * Purges the cache store, and loader if there is one.
2309
     *
2310
     * @return bool True on success, false otherwise
2311
     */
2312
    public function purge() {
2313
        $this->get_store()->purge();
2314
        if ($this->get_loader()) {
2315
            $this->get_loader()->purge();
2316
        }
2317
        return true;
2318
    }
2319
 
2320
    /**
2321
     * Test is a cache has a key.
2322
     *
2323
     * The use of the has methods is strongly discouraged. In a high load environment the cache may well change between the
2324
     * test and any subsequent action (get, set, delete etc).
2325
     * Instead it is recommended to write your code in such a way they it performs the following steps:
2326
     * <ol>
2327
     * <li>Attempt to retrieve the information.</li>
2328
     * <li>Generate the information.</li>
2329
     * <li>Attempt to set the information</li>
2330
     * </ol>
2331
     *
2332
     * Its also worth mentioning that not all stores support key tests.
2333
     * For stores that don't support key tests this functionality is mimicked by using the equivalent get method.
2334
     * Just one more reason you should not use these methods unless you have a very good reason to do so.
2335
     *
2336
     * @param string|int $key
2337
     * @param bool $tryloadifpossible If set to true, the cache doesn't contain the key, and there is another cache loader or
2338
     *      data source then the code will try load the key value from the next item in the chain.
2339
     * @return bool True if the cache has the requested key, false otherwise.
2340
     */
2341
    public function has($key, $tryloadifpossible = false) {
2342
        $this->check_tracked_user();
2343
        $parsedkey = $this->parse_key($key);
2344
        $store = $this->get_store();
2345
        if ($this->has_a_ttl() && !$this->store_supports_native_ttl()) {
2346
            // The data has a TTL and the store doesn't support it natively.
2347
            // We must fetch the data and expect a ttl wrapper.
2348
            $data = $store->get($parsedkey);
2349
            $has = ($data instanceof cache_ttl_wrapper && !$data->has_expired());
2350
        } else if (!$this->store_supports_key_awareness()) {
2351
            // The store doesn't support key awareness, get the data and check it manually... puke.
2352
            // Either no TTL is set of the store supports its handling natively.
2353
            $data = $store->get($parsedkey);
2354
            $has = ($data !== false);
2355
        } else {
2356
            // The store supports key awareness, this is easy!
2357
            // Either no TTL is set of the store supports its handling natively.
2358
            /* @var cache_store|cache_is_key_aware $store */
2359
            $has = $store->has($parsedkey);
2360
        }
2361
        if (!$has && $tryloadifpossible) {
2362
            $result = null;
2363
            if ($this->get_loader() !== false) {
2364
                $result = $this->get_loader()->get($parsedkey);
2365
            } else if ($this->get_datasource() !== null) {
2366
                $result = $this->get_datasource()->load_for_cache($key);
2367
            }
2368
            $has = ($result !== null);
2369
            if ($has) {
2370
                $this->set($key, $result);
2371
            }
2372
        }
2373
        return $has;
2374
    }
2375
 
2376
    /**
2377
     * Test is a cache has all of the given keys.
2378
     *
2379
     * It is strongly recommended to avoid the use of this function if not absolutely required.
2380
     * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc).
2381
     *
2382
     * Its also worth mentioning that not all stores support key tests.
2383
     * For stores that don't support key tests this functionality is mimicked by using the equivalent get method.
2384
     * Just one more reason you should not use these methods unless you have a very good reason to do so.
2385
     *
2386
     * @param array $keys
2387
     * @return bool True if the cache has all of the given keys, false otherwise.
2388
     */
2389
    public function has_all(array $keys) {
2390
        $this->check_tracked_user();
2391
        if (($this->has_a_ttl() && !$this->store_supports_native_ttl()) || !$this->store_supports_key_awareness()) {
2392
            foreach ($keys as $key) {
2393
                if (!$this->has($key)) {
2394
                    return false;
2395
                }
2396
            }
2397
            return true;
2398
        }
2399
        // The cache must be key aware and if support native ttl if it a ttl is set.
2400
        /* @var cache_store|cache_is_key_aware $store */
2401
        $store = $this->get_store();
2402
        return $store->has_all(array_map(array($this, 'parse_key'), $keys));
2403
    }
2404
 
2405
    /**
2406
     * Test if a cache has at least one of the given keys.
2407
     *
2408
     * It is strongly recommended to avoid the use of this function if not absolutely required.
2409
     * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc).
2410
     *
2411
     * Its also worth mentioning that not all stores support key tests.
2412
     * For stores that don't support key tests this functionality is mimicked by using the equivalent get method.
2413
     * Just one more reason you should not use these methods unless you have a very good reason to do so.
2414
     *
2415
     * @param array $keys
2416
     * @return bool True if the cache has at least one of the given keys
2417
     */
2418
    public function has_any(array $keys) {
2419
        if (($this->has_a_ttl() && !$this->store_supports_native_ttl()) || !$this->store_supports_key_awareness()) {
2420
            foreach ($keys as $key) {
2421
                if ($this->has($key)) {
2422
                    return true;
2423
                }
2424
            }
2425
            return false;
2426
        }
2427
        /* @var cache_store|cache_is_key_aware $store */
2428
        $store = $this->get_store();
2429
        return $store->has_any(array_map(array($this, 'parse_key'), $keys));
2430
    }
2431
 
2432
    /**
2433
     * The session loader never uses static acceleration.
2434
     * Instead it stores things in the static $session variable. Shared between all session loaders.
2435
     *
2436
     * @return bool
2437
     */
2438
    protected function use_static_acceleration() {
2439
        return false;
2440
    }
2441
}
2442
 
2443
/**
2444
 * An request cache.
2445
 *
2446
 * This class is used for request caches returned by the cache::make methods.
2447
 *
2448
 * This cache class should never be interacted with directly. Instead you should always use the cache::make methods.
2449
 * It is technically possible to call those methods through this class however there is no guarantee that you will get an
2450
 * instance of this class back again.
2451
 *
2452
 * @internal don't use me directly.
2453
 *
2454
 * @package    core
2455
 * @category   cache
2456
 * @copyright  2012 Sam Hemelryk
2457
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2458
 */
2459
class cache_request extends cache {
2460
    // This comment appeases code pre-checker ;) !
2461
}