Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 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
namespace core_cache;
18
 
19
use core\exception\coding_exception;
20
use core\exception\moodle_exception;
21
 
22
/**
23
 * An application cache.
24
 *
25
 * This class is used for application caches returned by the cache::make methods.
26
 * On top of the standard functionality it also allows locking to be required and or manually operated.
27
 *
28
 * This cache class should never be interacted with directly. Instead you should always use the cache::make methods.
29
 * It is technically possible to call those methods through this class however there is no guarantee that you will get an
30
 * instance of this class back again.
31
 *
32
 * @internal don't use me directly.
33
 *
34
 * @package    core_cache
35
 * @category   cache
36
 * @copyright  2012 Sam Hemelryk
37
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38
 */
39
class application_cache extends cache implements loader_with_locking_interface {
40
    /**
41
     * Lock identifier.
42
     * This is used to ensure the lock belongs to the cache instance + definition + user.
43
     * @var string
44
     */
45
    protected $lockidentifier;
46
 
47
    /**
48
     * Gets set to true if the cache's primary store natively supports locking.
49
     * If it does then we use that, otherwise we need to instantiate a second store to use for locking.
50
     * @var store
51
     */
52
    protected $nativelocking = null;
53
 
54
    /**
55
     * Gets set to true if the cache is going to be using locking.
56
     * This isn't a requirement, it doesn't need to use locking (most won't) and this bool is used to quickly check things.
57
     * If required then locking will be forced for the get|set|delete operation.
58
     * @var bool
59
     */
60
    protected $requirelocking = false;
61
 
62
    /**
63
     * Gets set to true if the cache writes (set|delete) must have a manual lock created first
64
     * @var bool
65
     */
66
    protected $requirelockingbeforewrite = false;
67
 
68
    /**
69
     * Gets set to a store to use for locking if the caches primary store doesn't support locking natively.
70
     * @var lockable_cache_interface
71
     */
72
    protected $cachelockinstance;
73
 
74
    /**
75
     * Store a list of locks acquired by this process.
76
     * @var array
77
     */
78
    protected $locks;
79
 
80
    /**
81
     * Overrides the cache construct method.
82
     *
83
     * You should not call this method from your code, instead you should use the cache::make methods.
84
     *
85
     * @param definition $definition
86
     * @param store $store
87
     * @param loader_interface|data_source_interface $loader
88
     */
89
    public function __construct(definition $definition, store $store, $loader = null) {
90
        parent::__construct($definition, $store, $loader);
91
        $this->nativelocking = $this->store_supports_native_locking();
92
        if ($definition->require_locking()) {
93
            $this->requirelocking = true;
94
            $this->requirelockingbeforewrite = $definition->require_locking_before_write();
95
        }
96
 
97
        $this->handle_invalidation_events();
98
    }
99
 
100
    /**
101
     * Returns the identifier to use
102
     *
103
     * @staticvar int $instances Counts the number of instances. Used as part of the lock identifier.
104
     * @return string
105
     */
106
    public function get_identifier() {
107
        static $instances = 0;
108
        if ($this->lockidentifier === null) {
109
            $this->lockidentifier = md5(
110
                $this->get_definition()->generate_definition_hash() .
111
                sesskey() .
112
                $instances++ .
113
                application_cache::class,
114
            );
115
        }
116
        return $this->lockidentifier;
117
    }
118
 
119
    /**
120
     * Fixes the instance up after a clone.
121
     */
122
    public function __clone() {
123
        // Force a new idenfitier.
124
        $this->lockidentifier = null;
125
    }
126
 
127
    /**
128
     * Acquires a lock on the given key.
129
     *
130
     * This is done automatically if the definition requires it.
131
     * It is recommended to use a definition if you want to have locking although it is possible to do locking without having
132
     * it required by the definition.
133
     * The problem with such an approach is that you cannot ensure that code will consistently use locking. You will need to
134
     * rely on the integrators review skills.
135
     *
136
     * @param string|int $key The key as given to get|set|delete
137
     * @return bool Always returns true
138
     * @throws moodle_exception If the lock cannot be obtained
139
     */
140
    public function acquire_lock($key) {
141
        $releaseparent = false;
142
        try {
143
            if ($this->get_loader() !== false) {
144
                $this->get_loader()->acquire_lock($key);
145
                // We need to release this lock later if the lock is not successful.
146
                $releaseparent = true;
147
            }
148
            $hashedkey = helper::hash_key($key, $this->get_definition());
149
            $before = microtime(true);
150
            if ($this->nativelocking) {
151
                $lock = $this->get_store()->acquire_lock($hashedkey, $this->get_identifier());
152
            } else {
153
                $this->ensure_cachelock_available();
154
                $lock = $this->cachelockinstance->lock($hashedkey, $this->get_identifier());
155
            }
156
            $after = microtime(true);
157
            if ($lock) {
158
                $this->locks[$hashedkey] = $lock;
159
                if (MDL_PERF || $this->perfdebug) {
160
                    \core\lock\timing_wrapper_lock_factory::record_lock_data(
161
                        $after,
162
                        $before,
163
                        $this->get_definition()->get_id(),
164
                        $hashedkey,
165
                        $lock,
166
                        $this->get_identifier() . $hashedkey
167
                    );
168
                }
169
                $releaseparent = false;
170
                return true;
171
            } else {
172
                throw new moodle_exception(
173
                    'ex_unabletolock',
174
                    'cache',
175
                    '',
176
                    null,
177
                    'store: ' . get_class($this->get_store()) . ', lock: ' . $hashedkey
178
                );
179
            }
180
        } finally {
181
            // Release the parent lock if we acquired it, then threw an exception.
182
            if ($releaseparent) {
183
                $this->get_loader()->release_lock($key);
184
            }
185
        }
186
    }
187
 
188
    /**
189
     * Checks if this cache has a lock on the given key.
190
     *
191
     * @param string|int $key The key as given to get|set|delete
192
     * @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
193
     *      someone else has the lock.
194
     */
195
    public function check_lock_state($key) {
196
        $key = helper::hash_key($key, $this->get_definition());
197
        if (!empty($this->locks[$key])) {
198
            return true; // Shortcut to save having to make a call to the cache store if the lock is held by this process.
199
        }
200
        if ($this->nativelocking) {
201
            return $this->get_store()->check_lock_state($key, $this->get_identifier());
202
        } else {
203
            $this->ensure_cachelock_available();
204
            return $this->cachelockinstance->check_state($key, $this->get_identifier());
205
        }
206
    }
207
 
208
    /**
209
     * Releases the lock this cache has on the given key
210
     *
211
     * @param string|int $key
212
     * @return bool True if the operation succeeded, false otherwise.
213
     */
214
    public function release_lock($key) {
215
        $loaderkey = $key;
216
        $key = helper::hash_key($key, $this->get_definition());
217
        if ($this->nativelocking) {
218
            $released = $this->get_store()->release_lock($key, $this->get_identifier());
219
        } else {
220
            $this->ensure_cachelock_available();
221
            $released = $this->cachelockinstance->unlock($key, $this->get_identifier());
222
        }
223
        if ($released && array_key_exists($key, $this->locks)) {
224
            unset($this->locks[$key]);
225
            if (MDL_PERF || $this->perfdebug) {
226
                \core\lock\timing_wrapper_lock_factory::record_lock_released_data($this->get_identifier() . $key);
227
            }
228
        }
229
        if ($this->get_loader() !== false) {
230
            $this->get_loader()->release_lock($loaderkey);
231
        }
232
        return $released;
233
    }
234
 
235
    /**
236
     * Ensure that the dedicated lock store is ready to go.
237
     *
238
     * This should only happen if the cache store doesn't natively support it.
239
     */
240
    protected function ensure_cachelock_available() {
241
        if ($this->cachelockinstance === null) {
242
            $this->cachelockinstance = helper::get_cachelock_for_store($this->get_store());
243
        }
244
    }
245
 
246
    /**
247
     * Sends a key => value pair to the cache.
248
     *
249
     * <code>
250
     * // This code will add four entries to the cache, one for each url.
251
     * $cache->set('main', 'http://moodle.org');
252
     * $cache->set('docs', 'http://docs.moodle.org');
253
     * $cache->set('tracker', 'http://tracker.moodle.org');
254
     * $cache->set('qa', 'http://qa.moodle.net');
255
     * </code>
256
     *
257
     * @param string|int $key The key for the data being requested.
258
     * @param int $version Version number
259
     * @param mixed $data The data to set against the key.
260
     * @param bool $setparents If true, sets all parent loaders, otherwise only this one
261
     * @return bool True on success, false otherwise.
262
     * @throws coding_exception If a required lock has not beeen acquired
263
     */
264
    protected function set_implementation($key, int $version, $data, bool $setparents = true): bool {
265
        if ($this->requirelockingbeforewrite && !$this->check_lock_state($key)) {
266
            throw new coding_exception('Attempted to set cache key "' . $key . '" without a lock. '
267
                . 'Locking before writes is required for ' . $this->get_definition()->get_id());
268
        }
269
        return parent::set_implementation($key, $version, $data, $setparents);
270
    }
271
 
272
    /**
273
     * Sends several key => value pairs to the cache.
274
     *
275
     * Using this function comes with potential performance implications.
276
     * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call
277
     * the equivalent singular method for each item provided.
278
     * This should not deter you from using this function as there is a performance benefit in situations where the cache store
279
     * does support it, but you should be aware of this fact.
280
     *
281
     * <code>
282
     * // This code will add four entries to the cache, one for each url.
283
     * $cache->set_many(array(
284
     *     'main' => 'http://moodle.org',
285
     *     'docs' => 'http://docs.moodle.org',
286
     *     'tracker' => 'http://tracker.moodle.org',
287
     *     'qa' => ''http://qa.moodle.net'
288
     * ));
289
     * </code>
290
     *
291
     * @param array $keyvaluearray An array of key => value pairs to send to the cache.
292
     * @return int The number of items successfully set. It is up to the developer to check this matches the number of items.
293
     *      ... if they care that is.
294
     * @throws coding_exception If a required lock has not beeen acquired
295
     */
296
    public function set_many(array $keyvaluearray) {
297
        if ($this->requirelockingbeforewrite) {
298
            foreach ($keyvaluearray as $key => $value) {
299
                if (!$this->check_lock_state($key)) {
300
                    throw new coding_exception('Attempted to set cache key "' . $key . '" without a lock. '
301
                            . 'Locking before writes is required for ' . $this->get_definition()->get_id());
302
                }
303
            }
304
        }
305
        return parent::set_many($keyvaluearray);
306
    }
307
 
308
    /**
309
     * Delete the given key from the cache.
310
     *
311
     * @param string|int $key The key to delete.
312
     * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores.
313
     *     This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this.
314
     * @return bool True of success, false otherwise.
315
     * @throws coding_exception If a required lock has not beeen acquired
316
     */
317
    public function delete($key, $recurse = true) {
318
        if ($this->requirelockingbeforewrite && !$this->check_lock_state($key)) {
319
            throw new coding_exception('Attempted to delete cache key "' . $key . '" without a lock. '
320
                    . 'Locking before writes is required for ' . $this->get_definition()->get_id());
321
        }
322
        return parent::delete($key, $recurse);
323
    }
324
 
325
    /**
326
     * Delete all of the given keys from the cache.
327
     *
328
     * @param array $keys The key to delete.
329
     * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores.
330
     *     This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this.
331
     * @return int The number of items successfully deleted.
332
     * @throws coding_exception If a required lock has not beeen acquired
333
     */
334
    public function delete_many(array $keys, $recurse = true) {
335
        if ($this->requirelockingbeforewrite) {
336
            foreach ($keys as $key) {
337
                if (!$this->check_lock_state($key)) {
338
                    throw new coding_exception('Attempted to delete cache key "' . $key . '" without a lock. '
339
                            . 'Locking before writes is required for ' . $this->get_definition()->get_id());
340
                }
341
            }
342
        }
343
        return parent::delete_many($keys, $recurse);
344
    }
345
}
346
 
347
// Alias this class to the old name.
348
// This file will be autoloaded by the legacyclasses autoload system.
349
// In future all uses of this class will be corrected and the legacy references will be removed.
350
class_alias(application_cache::class, \cache_application::class);