Completed
Branch BUG/11167/fix-runaway-ee-trans... (3205ed)
by
unknown
102:42 queued 91:11
created

TransientCacheStorage::deleteTransientKeys()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 23
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 17
nc 3
nop 2
dl 0
loc 23
rs 8.5906
c 0
b 0
f 0
1
<?php
2
namespace EventEspresso\core\services\cache;
3
4
defined('EVENT_ESPRESSO_VERSION') || exit;
5
6
7
8
/**
9
 * Class TransientCacheStorage
10
 * Manages the creation and cleanup of transients
11
 * by tracking transient keys and their corresponding expiration.
12
 * The transient cleanup schedule is filterable
13
 * to control how often cleanup occurs
14
 *
15
 * @package       Event Espresso
16
 * @author        Brent Christensen
17
 * @since         4.9.31
18
 */
19
class TransientCacheStorage implements CacheStorageInterface
20
{
21
22
    /**
23
     * wp-option option_name for tracking transients
24
     *
25
     * @type string
26
     */
27
    const TRANSIENT_SCHEDULE_OPTIONS_KEY = 'ee_transient_schedule';
28
29
    /**
30
     * @var int $current_time
31
     */
32
    private $current_time;
33
34
    /**
35
     * how often to perform transient cleanup
36
     *
37
     * @var string $transient_cleanup_frequency
38
     */
39
    private $transient_cleanup_frequency;
40
41
    /**
42
     * options for how often to perform transient cleanup
43
     *
44
     * @var array $transient_cleanup_frequency_options
45
     */
46
    private $transient_cleanup_frequency_options = array();
47
48
    /**
49
     * @var array $transients
50
     */
51
    private $transients;
52
53
54
55
    /**
56
     * TransientCacheStorage constructor.
57
     */
58
    public function __construct()
59
    {
60
        $this->transient_cleanup_frequency = $this->setTransientCleanupFrequency();
61
        // round current time down to closest 5 minutes to simplify scheduling
62
        $this->current_time = $this->roundTimestamp(time(), '5-minutes', false);
63
        $this->transients = (array)get_option(TransientCacheStorage::TRANSIENT_SCHEDULE_OPTIONS_KEY, array());
64
        if ( ! (defined('DOING_AJAX') && DOING_AJAX) && $this->transient_cleanup_frequency !== 'off') {
65
            add_action('shutdown', array($this, 'checkTransientCleanupSchedule'), 999);
66
        }
67
    }
68
69
70
71
    /**
72
     * Sets how often transient cleanup occurs
73
     *
74
     * @return string
75
     */
76
    private function setTransientCleanupFrequency()
77
    {
78
        // sets how often transients are cleaned up
79
        $this->transient_cleanup_frequency_options = apply_filters(
80
            'FHEE__TransientCacheStorage__transient_cleanup_schedule_options',
81
            array(
82
                'off',
83
                '15-minutes',
84
                'hour',
85
                '12-hours',
86
                'day',
87
            )
88
        );
89
        $transient_cleanup_frequency = apply_filters(
90
            'FHEE__TransientCacheStorage__transient_cleanup_schedule',
91
            'hour'
92
        );
93
        return in_array(
94
            $transient_cleanup_frequency,
95
            $this->transient_cleanup_frequency_options,
96
            true
97
        )
98
            ? $transient_cleanup_frequency
99
            : 'hour';
100
    }
101
102
103
104
    /**
105
     * we need to be able to round timestamps off to match the set transient cleanup frequency
106
     * so if a transient is set to expire at 1:17 pm for example, and our cleanup schedule is every hour,
107
     * then that timestamp needs to be rounded up to 2:00 pm so that it is removed
108
     * during the next scheduled cleanup after its expiration.
109
     * We also round off the current time timestamp to the closest 5 minutes
110
     * just to make the timestamps a little easier to round which helps with debugging.
111
     *
112
     * @param int    $timestamp [required]
113
     * @param string $cleanup_frequency
114
     * @param bool   $round_up
115
     * @return int
116
     */
117
    private function roundTimestamp($timestamp, $cleanup_frequency = 'hour', $round_up = true)
118
    {
119
        $cleanup_frequency = $cleanup_frequency ? $cleanup_frequency : $this->transient_cleanup_frequency;
120
        // in order to round the time to the closest xx minutes (or hours),
121
        // we take the minutes (or hours) portion of the timestamp and divide it by xx,
122
        // round down to a whole number, then multiply by xx to bring us almost back up to where we were
123
        // why round down ? so the minutes (or hours) don't go over 60 (or 24)
124
        // and bump the hour, which could bump the day, which could bump the month, etc,
125
        // which would be bad because we don't always want to round up,
126
        // but when we do we can easily achieve that by simply adding the desired offset,
127
        $minutes = '00';
128
        $hours = 'H';
129
        switch ($cleanup_frequency) {
130 View Code Duplication
            case '5-minutes' :
131
                $minutes = floor((int)date('i', $timestamp) / 5) * 5;
132
                $minutes = str_pad($minutes, 2, '0', STR_PAD_LEFT);
133
                $offset = MINUTE_IN_SECONDS * 5;
134
                break;
135 View Code Duplication
            case '15-minutes' :
136
                $minutes = floor((int)date('i', $timestamp) / 15) * 15;
137
                $minutes = str_pad($minutes, 2, '0', STR_PAD_LEFT);
138
                $offset = MINUTE_IN_SECONDS * 15;
139
                break;
140 View Code Duplication
            case '12-hours' :
141
                $hours = floor((int)date('H', $timestamp) / 12) * 12;
142
                $hours = str_pad($hours, 2, '0', STR_PAD_LEFT);
143
                $offset = HOUR_IN_SECONDS * 12;
144
                break;
145
            case 'day' :
146
                $hours = '03'; // run cleanup at 3:00 am (or first site hit after that)
147
                $offset = DAY_IN_SECONDS;
148
                break;
149
            case 'hour' :
150
            default :
151
                $offset = HOUR_IN_SECONDS;
152
                break;
153
        }
154
        $rounded_timestamp = (int) strtotime(date("Y-m-d {$hours}:{$minutes}:00", $timestamp));
155
        $rounded_timestamp += $round_up ? $offset : 0;
156
        return apply_filters(
157
            'FHEE__TransientCacheStorage__roundTimestamp__timestamp',
158
            $rounded_timestamp,
159
            $timestamp,
160
            $cleanup_frequency,
161
            $round_up
162
        );
163
    }
164
165
166
167
    /**
168
     * Saves supplied data to a transient
169
     * if an expiration is set, then it automatically schedules the transient for cleanup
170
     *
171
     * @param string $transient_key [required]
172
     * @param string $data          [required]
173
     * @param int    $expiration    number of seconds until the cache expires
174
     * @return bool
175
     */
176
    public function add($transient_key, $data, $expiration = 0)
177
    {
178
        $expiration = (int)abs($expiration);
179
        $saved = set_transient($transient_key, $data, $expiration);
180
        if ($saved && $expiration) {
181
            $this->scheduleTransientCleanup($transient_key, $expiration);
182
        }
183
        return $saved;
184
    }
185
186
187
188
    /**
189
     * retrieves transient data
190
     * automatically triggers early cache refresh for standard cache items
191
     * in order to avoid cache stampedes on busy sites.
192
     * For non-standard cache items like PHP Session data where early refreshing is not wanted,
193
     * the $standard_cache parameter should be set to false when retrieving data
194
     *
195
     * @param string $transient_key [required]
196
     * @param bool   $standard_cache
197
     * @return mixed|null
198
     */
199
    public function get($transient_key, $standard_cache = true)
200
    {
201
        if (isset($this->transients[ $transient_key ])) {
202
            // to avoid cache stampedes (AKA:dogpiles) for standard cache items,
203
            // check if known cache expires within the next minute,
204
            // and if so, remove it from our tracking and and return nothing.
205
            // this should trigger the cache content to be regenerated during this request,
206
            // while allowing any following requests to still access the existing cache
207
            // until it gets replaced with the refreshed content
208
            if (
209
                $standard_cache
210
                && $this->transients[$transient_key] - time() <= MINUTE_IN_SECONDS
211
            ) {
212
                unset($this->transients[$transient_key]);
213
                $this->updateTransients();
214
                return null;
215
            }
216
217
            // for non standard cache items, remove the key from our tracking,
218
            // but proceed to retrieve the transient so that it also gets removed from the db
219
            if ($this->transients[$transient_key] <= time()) {
220
                unset($this->transients[$transient_key]);
221
                $this->updateTransients();
222
            }
223
        }
224
225
        $content = get_transient($transient_key);
226
        return $content !== false ? $content : null;
227
    }
228
229
230
231
    /**
232
     * delete a single transient and remove tracking
233
     *
234
     * @param string $transient_key [required] full or partial transient key to be deleted
235
     */
236
    public function delete($transient_key)
237
    {
238
        $this->deleteMany(array($transient_key));
239
    }
240
241
242
243
    /**
244
     * delete multiple transients and remove tracking
245
     *
246
     * @param array $transient_keys [required] array of full or partial transient keys to be deleted
247
     * @param bool  $force_delete   [optional] if true, then will not check incoming keys against those being tracked
248
     *                              and proceed directly to deleting those entries from the cache storage
249
     */
250
    public function deleteMany(array $transient_keys, $force_delete = false)
251
    {
252
        $full_transient_keys = $force_delete ? $transient_keys : array();
253
        if(empty($full_transient_keys)){
254
            foreach ($this->transients as $transient_key => $expiration) {
255
                foreach ($transient_keys as $transient_key_to_delete) {
256
                    if (strpos($transient_key, $transient_key_to_delete) !== false) {
257
                        $full_transient_keys[] = $transient_key;
258
                    }
259
                }
260
            }
261
        }
262
        if ($this->deleteTransientKeys($full_transient_keys)) {
263
            $this->updateTransients();
264
        }
265
    }
266
267
268
269
    /**
270
     * sorts transients numerically by timestamp
271
     * then saves the transient schedule to a WP option
272
     */
273
    private function updateTransients()
274
    {
275
        asort($this->transients, SORT_NUMERIC);
276
        update_option(
277
            TransientCacheStorage::TRANSIENT_SCHEDULE_OPTIONS_KEY,
278
            $this->transients
279
        );
280
    }
281
282
283
284
    /**
285
     * schedules a transient for cleanup by adding it to the transient tracking
286
     *
287
     * @param string $transient_key [required]
288
     * @param int    $expiration    [required]
289
     */
290
    private function scheduleTransientCleanup($transient_key, $expiration)
291
    {
292
        // make sure a valid future timestamp is set
293
        $expiration += $expiration < time() ? time() : 0;
294
        // and round to the closest 15 minutes
295
        $expiration = $this->roundTimestamp($expiration);
296
        // save transients to clear using their ID as the key to avoid duplicates
297
        $this->transients[$transient_key] = $expiration;
298
        $this->updateTransients();
299
    }
300
301
302
303
    /**
304
     * Since our tracked transients are sorted by their timestamps
305
     * we can grab the first transient and see when it is scheduled for cleanup.
306
     * If that timestamp is less than or equal to the current time,
307
     * then cleanup is triggered
308
     */
309
    public function checkTransientCleanupSchedule()
310
    {
311
        if (empty($this->transients)) {
312
            return;
313
        }
314
        // when do we run the next cleanup job?
315
        reset($this->transients);
316
        $next_scheduled_cleanup = current($this->transients);
317
        // if the next cleanup job is scheduled for the current hour
318
        if ($next_scheduled_cleanup <= $this->current_time) {
319
            if ($this->cleanupExpiredTransients()) {
320
                $this->updateTransients();
321
            }
322
        }
323
    }
324
325
326
327
    /**
328
     * loops through the array of tracked transients,
329
     * compiles a list of those that have expired, and sends that list off for deletion.
330
     * Also removes any bad records from the transients array
331
     *
332
     * @return bool
333
     */
334
    private function cleanupExpiredTransients()
335
    {
336
        $update = false;
337
        // filter the query limit. Set to 0 to turn off garbage collection
338
        $limit = (int)abs(
339
            apply_filters(
340
                'FHEE__TransientCacheStorage__clearExpiredTransients__limit',
341
                50
342
            )
343
        );
344
        // non-zero LIMIT means take out the trash
345
        if ($limit) {
346
            $transient_keys = array();
347
            foreach ($this->transients as $transient_key => $expiration) {
348
                if ($expiration > $this->current_time) {
349
                    continue;
350
                }
351
                if ( ! $expiration || ! $transient_key) {
352
                    unset($this->transients[$transient_key]);
353
                    $update = true;
354
                    continue;
355
                }
356
                $transient_keys[] = $transient_key;
357
            }
358
            // delete expired keys, but maintain value of $update if nothing is deleted
359
            $update = $this->deleteTransientKeys($transient_keys, $limit) ? true : $update;
360
            do_action( 'FHEE__TransientCacheStorage__clearExpiredTransients__end', $this);
361
        }
362
        return $update;
363
    }
364
365
366
367
    /**
368
     * calls delete_transient() on each transient key provided, up to the specified limit
369
     *
370
     * @param array $transient_keys [required]
371
     * @param int   $limit
372
     * @return bool
373
     */
374
    private function deleteTransientKeys(array $transient_keys, $limit = 50)
375
    {
376
        if (empty($transient_keys)) {
377
            return false;
378
        }
379
        $counter = 0;
380
        foreach ($transient_keys as $transient_key) {
381
            if($counter === $limit){
382
                break;
383
            }
384
            // remove any transient prefixes
385
            $transient_key = strpos($transient_key,  '_transient_timeout_') === 0
386
                ? str_replace('_transient_timeout_', '', $transient_key)
387
                : $transient_key;
388
            $transient_key = strpos($transient_key,  '_transient_') === 0
389
                ? str_replace('_transient_', '', $transient_key)
390
                : $transient_key;
391
            delete_transient($transient_key);
392
            unset($this->transients[$transient_key]);
393
            $counter++;
394
        }
395
        return $counter > 0;
396
    }
397
398
399
400
}
401
// End of file TransientCacheStorage.php
402
// Location: EventEspresso\core\services\cache/TransientCacheStorage.php
403