PluginProbe
SQLite Object Cache / 1.6.0
SQLite Object Cache v1.6.0
1.6.5 trunk 0.1.7 1.0.0 1.1.0 1.1.1 1.2.0 1.2.1 1.2.2 1.2.3 1.3.0 1.3.1 1.3.2 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.4.0 1.4.1 1.5.1 1.5.4 1.5.5 1.5.6 1.5.7 All 30 releases
← All changes | assets/drop-in/object-cache.php +2833 -2185 1.2.31.6.0 View file →
@@ -1,8 +1,8 @@
1 1 <?php
2 2 /**
3 3 * Plugin Name: SQLite Object Cache (Drop-in)
4 - * Version: 1.2.3
4 + * Version: 1.6.0
5 5 * Note: This Version number must match the one in SQLite_Object_Cache::_construct.
6 6 * Plugin URI: https://wordpress.org/plugins/sqlite-object-cache/
7 7 * Description: A persistent object cache backend powered by SQLite3.
8 8 * Author: Oliver Jones
@@ -9,23 +9,30 @@
9 9 * Author URI: https://plumislandmedia.net
10 10 * License: GPLv2+
11 11 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
12 12 * Requires PHP: 5.6
13 + * Tested up to: 6.9
14 + * Stable tag: 1.6.0
13 15 *
14 16 * NOTE: This uses the file .../wp-content/.ht.object_cache.sqlite
15 17 * and the associated files .../wp-content/.ht.object_cache.sqlite-shm
16 18 * and .../wp-content/.ht.object_cache.sqlite-wal to hold cached data.
17 - * These start with .ht. for security: Most web servers block requests
19 + * These start with .ht. for security: Many web servers block requests
18 20 * for files with that prefix. Use the UNIX ls -a command to
19 21 * see these files from your command line.
20 22 *
21 23 * Some config settings control this.
24 + * WP_SQLITE_OBJECT_CACHE_APCU, if true, enables cache acceleration with APCu RAM. This setting can be updated from the plugin's Settings page.
22 25 * WP_SQLITE_OBJECT_CACHE_DB_FILE, if defined, is the cache file path.
23 26 * /var/tmp/cache.sqlite puts the cache file outside the document root.
24 - * WP_CACHE_KEY_SALT is used as part of the cache file.
27 + * WP_CACHE_KEY_SALT, if present, is used as part of the cache file name, and as a prefix for APCu keys.
25 28 * WP_SQLITE_OBJECT_CACHE_TIMEOUT is the SQLite timeout in place of 5000 milliseconds.
29 + * WP_SQLITE_OBJECT_CACHE_SERIALIZE, if true, requires the use of php serialize.
26 30 * WP_SQLITE_OBJECT_CACHE_JOURNAL_MODE is the SQLite journal mode in place of 'WAL'.
27 - * It can be DELETE | TRUNCATE | PERSIST | MEMORY | WAL. See https://www.sqlite.org/pragma.html#pragma_journal_mode
31 + * It can be DELETE | TRUNCATE | PERSIST | MEMORY | WAL. See https://www.sqlite.org/pragma.html#pragma_journal_mode.
32 + * WP_SQLITE_OBJECT_CACHE_INTKEY_LENGTH is the number of digits for optimizing consecutive integer cache keys, default 6.
33 + * WP_SQLITE_OBJECT_CACHE_INTKEY_ERODE_GAPS allows fewer SQL statements but can retrieve extra items, default 2.
34 + * WP_SQLITE_OBJECT_CACHE_MMAP_SIZE sets SQLite's mmap_size in MiB. Default 0: disabled.
28 35 *
29 36 * Credit: Till Krüss's https://wordpress.org/plugins/redis-cache/ plugin. Thanks, Till!
30 37 *
31 38 * @package SQLiteCache
@@ -30,2440 +37,3081 @@
30 37 *
31 38 * @package SQLiteCache
32 39 */
33 40
41 +/** @noinspection SqlDialectInspection */
42 +
43 +use JetBrains\PhpStorm\NoReturn;
44 +
34 45 defined( '\\ABSPATH' ) || exit;
35 46
47 +/**
48 + * hrtime polyfill if needed, pre php 7.3.
49 + */
50 +if ( ! function_exists( 'hrtime' ) ) {
51 + function hrtime( $as_float = false ) {
52 + if ( $as_float ) {
53 + return microtime( true ) * 1000;
54 + }
55 + $result = microtime( false );
56 + $result[1] = 1000 * $result [1];
57 + return $result;
58 + }
59 +}
60 +
36 61 // phpcs:disable Generic.WhiteSpace.ScopeIndent.IncorrectExact, Generic.WhiteSpace.ScopeIndent.Incorrect
37 62 if ( ! defined( 'WP_SQLITE_OBJECT_CACHE_DISABLED' ) || ! WP_SQLITE_OBJECT_CACHE_DISABLED ) :
38 63
39 - /**
40 - * Object Cache API: WP_Object_Cache class, reworked for SQLite3 drop-in.
41 - *
42 - * @package WordPress
43 - * @subpackage Cache
44 - * @since 5.4.0
45 - */
64 + /**
65 + * Object Cache API: WP_Object_Cache class, reworked for SQLite3 drop-in.
66 + *
67 + * NOTE WELL: SQL in this file is not for use with $wpdb, but for SQLite3.
68 + *
69 + * @package WordPress
70 + * @subpackage Cache
71 + * @since 5.4.0
72 + */
46 73
47 - /**
48 - * Core class that implements an object cache.
49 - *
50 - * The WordPress Object Cache is used to save on trips to the database. The
51 - * Object Cache stores cache data to memory and makes the cache
52 - * contents available by using a key, which is used to name and later retrieve
53 - * the cache contents.
54 - *
55 - * This module is a drop-in, placed in the WP_CONTENT folder, implementing
56 - * the WordPress Object Cache class, while using SQLite3 for persistent storage.
57 - *
58 - * @since 0.1.0
59 - */
60 - class WP_Object_Cache {
61 - const OBJECT_STATS_TABLE = 'object_stats';
62 - const OBJECT_CACHE_TABLE = 'object_cache';
63 - const NOEXPIRE_TIMESTAMP_OFFSET = 500000000000;
64 - const MAX_LIFETIME = DAY_IN_SECONDS * 2;
65 - const SQLITE_TIMEOUT = 5000;
66 - const SQLITE_FILENAME = '.ht.object-cache.sqlite';
67 - const JOURNAL_MODE = 'WAL'; /* or 'MEMORY' */
74 + /**
75 + * Core class that implements an object cache.
76 + *
77 + * The WordPress Object Cache is used to save on trips to the database. The
78 + * Object Cache stores cache data to memory and makes the cache
79 + * contents available by using a key, which is used to name and later retrieve
80 + * the cache contents.
81 + *
82 + * This module is a drop-in, placed in the WP_CONTENT folder, implementing
83 + * the WordPress Object Cache class, while using SQLite3 for persistent storage.
84 + *
85 + * @since 0.1.0
86 + */
87 + class WP_Object_Cache {
88 + const OBJECT_STATS_TABLE = 'object_stats';
89 + const OBJECT_CACHE_TABLE = 'object_cache';
90 + const OBJECT_FLAGS_TABLE = 'object_flags';
91 + const NOEXPIRE_TIMESTAMP_OFFSET = 500000000000;
92 + const INTKEY_LENGTH = 6;
93 + const MMAP_SIZE = 0.0;
94 + const INTKEY_ERODE_GAPS = 2;
95 + const INTKEY_SENTINEL = "\x1f"; /* Only one character allowed here. */
96 + const SQLITE_TIMEOUT = 5000;
97 + const SQLITE_FILENAME = '.ht.object-cache.sqlite';
98 + const JOURNAL_MODE = 'WAL'; /* or 'MEMORY' */
99 + const TRANSACTION_SIZE_LIMIT = 64;
68 100
69 - /**
70 - * @var bool True if a transaction is active.
71 - */
72 - private $transaction_active = false;
73 - /**
74 - * Path to SQLite file.
75 - *
76 - * @var string
77 - */
78 - public $sqlite_path;
101 + private $dropin_version = '1.6.0';
102 + /** @var bool True if a transaction is active. */
103 + private $transaction_active = false;
104 + /** Path to SQLite file. @var string */
105 + public $sqlite_path;
79 106
80 - /**
81 - * SQLite's journal mode.
82 - *
83 - * Avoid the OFF journal mode, especially in pre-3.24 versions of SQLite.
84 - *
85 - * @see https://www.sqlite.org/pragma.html#pragma_journal_mode
86 - *
87 - * @var string MEMORY, WAL, DELETE, TRUNCATE, PERSIST, OFF
88 - */
89 - private $sqlite_journal_mode;
90 - /**
91 - * Timeout waiting for transaction completion.
92 - *
93 - * @var int
94 - */
95 - private $sqlite_timeout;
96 - /**
97 - * The amount of times the cache data was already stored in the cache.
98 - *
99 - * @since 2.5.0
100 - * @var int
101 - */
102 - public $cache_hits = 0;
103 - /**
104 - * Amount of times the cache did not have the request in cache.
105 - *
106 - * @since 2.0.0
107 - * @var int
108 - */
109 - public $cache_misses = 0;
110 - /**
111 - * The amount of times the cache data was already stored in the persistent cache.
112 - *
113 - * @since 2.5.0
114 - * @var int
115 - */
116 - public $persistent_hits = 0;
117 - /**
118 - * Amount of times the cache did not have the request in persistent cache.
119 - *
120 - * @since 2.0.0
121 - * @var int
122 - */
123 - public $persistent_misses = 0;
124 - /**
125 - * The blog prefix to prepend to keys in non-global groups.
126 - *
127 - * @since 3.5.0
128 - * @var string
129 - */
130 - public $blog_prefix;
131 - /**
132 - * List of groups that will not be flushed.
133 - *
134 - * @var array
135 - */
136 - public $unflushable_groups = [];
137 - /**
138 - * List of groups not saved to cache.
139 - *
140 - * @var array
141 - */
142 - public $ignored_groups = [
143 - 'counts',
144 - 'plugins',
145 - 'themes',
146 - ];
147 - /**
148 - * List of groups and their types.
149 - *
150 - * @var array
151 - */
152 - public $group_type = [];
153 - /**
154 - * Prefix used for global groups.
155 - *
156 - * @var string
157 - */
158 - public $global_prefix = '';
159 - /**
160 - * List of global groups.
161 - *
162 - * @var array
163 - */
164 - protected $global_groups = [
165 - 'blog-details',
166 - 'blog-id-cache',
167 - 'blog-lookup',
168 - 'global-posts',
169 - 'networks',
170 - 'rss',
171 - 'sites',
172 - 'site-details',
173 - 'site-lookup',
174 - 'site-options',
175 - 'site-transient',
176 - 'users',
177 - 'useremail',
178 - 'userlogins',
179 - 'usermeta',
180 - 'user_meta',
181 - 'userslugs',
182 - ];
183 - /**
184 - * Holds the cached objects.
185 - *
186 - * @since 2.0.0
187 - * @var array
188 - */
189 - private $cache = [];
190 - /**
191 - * Holds the value of is_multisite().
192 - *
193 - * @since 3.5.0
194 - * @var bool
195 - */
196 - private $multisite;
107 + /**
108 + * @var string|null Version of SQLite3 software in use.
109 + */
110 + private $sqlite_version;
197 111
198 - /**
199 - * Prepared statement to get one cache element.
200 - *
201 - * @var SQLite3Stmt SELECT statement.
202 - */
203 - private $getone;
112 + /**
113 + * SQLite's journal mode.
114 + *
115 + * Avoid the OFF journal mode, especially in pre-3.24 versions of SQLite.
116 + *
117 + * @see https://www.sqlite.org/pragma.html#pragma_journal_mode
118 + *
119 + * @var string MEMORY, WAL, DELETE, TRUNCATE, PERSIST, OFF
120 + */
121 + private $sqlite_journal_mode;
122 + /**
123 + * Timeout waiting for transaction completion.
124 + *
125 + * @var int
126 + */
127 + private $sqlite_timeout;
128 + /**
129 + * The amount of times the cache data was already stored in the cache.
130 + *
131 + * @since 2.5.0
132 + * @var int
133 + */
134 + public $cache_hits = 0;
135 + /**
136 + * Amount of times the cache did not have the request in cache.
137 + *
138 + * @since 2.0.0
139 + * @var int
140 + */
141 + public $cache_misses = 0;
142 + /**
143 + * The amount of times the cache data was already stored in the persistent cache.
144 + *
145 + * @since 2.5.0
146 + * @var int
147 + */
148 + public $persistent_hits = 0;
149 + /**
150 + * Amount of times the cache did not have the request in persistent cache.
151 + *
152 + * @since 2.0.0
153 + * @var int
154 + */
155 + public $persistent_misses = 0;
156 + /**
157 + * Amount of times the apcu cache had the item.
158 + *
159 + * @since 2.0.0
160 + * @var int
161 + */
162 + public $apcu_hits = 0;
163 + /**
164 + * Amount of times the apcu cache did not have the item.
165 + *
166 + * @since 2.0.0
167 + * @var int
168 + */
169 + public $apcu_misses = 0;
170 + /**
171 + * The blog prefix to prepend to keys in non-global groups.
172 + *
173 + * @since 3.5.0
174 + * @var string For multisite, n:, For single site, empty.
175 + */
176 + public $blog_prefix;
177 + /**
178 + * List of groups that will not be flushed.
179 + *
180 + * @var array
181 + */
182 + public $unflushable_groups = array();
183 + /**
184 + * List of groups not saved to cache.
185 + *
186 + * @var array
187 + */
188 + public $ignored_groups = array(
189 + 'counts',
190 + 'plugins',
191 + 'themes',
192 + );
193 + /**
194 + * List of groups and their types.
195 + *
196 + * @var array
197 + */
198 + public $group_type = array();
199 + /**
200 + * Prefix used for global groups.
201 + *
202 + * @var string
203 + */
204 + public $global_prefix = '';
205 + /**
206 + * List of global groups.
207 + *
208 + * @var array
209 + */
210 + protected $global_groups = array(
211 + 'blog-details',
212 + 'blog-id-cache',
213 + 'blog-lookup',
214 + 'global-posts',
215 + 'networks',
216 + 'rss',
217 + 'sites',
218 + 'site-details',
219 + 'site-lookup',
220 + 'site-options',
221 + 'site-transient',
222 + 'users',
223 + 'useremail',
224 + 'userlogins',
225 + 'usermeta',
226 + 'user_meta',
227 + 'userslugs',
228 + );
204 229
205 - /**
206 - * Prepared statement to delete one cache element.
207 - *
208 - * @var SQLite3Stmt DELETE statement.
209 - */
210 - private $deleteone;
230 + /**
231 + * @var array One-level associative array $name=>$value
232 + */
233 + private $cache = array();
234 + /**
235 + * Holds the value of is_multisite().
236 + *
237 + * @since 3.5.0
238 + * @var bool
239 + */
240 + private $multisite;
211 241
212 - /**
213 - * Prepared statement to delete a group of cache elements.
214 - *
215 - * @var SQLite3Stmt
216 - */
217 - private $deletegroup;
242 + /**
243 + * Prepared statement to get one cache element.
244 + *
245 + * @var SQLite3Stmt SELECT statement.
246 + */
247 + private $getone_stmt;
218 248
219 - /**
220 - * Prepared statement to upsert one cache element.
221 - *
222 - * @var SQLite3Stmt
223 - */
224 - private $upsertone;
249 + /**
250 + * Prepared statement to get a range of cache elements, for get_multiple.
251 + *
252 + * @var SQLite3Stmt SELECT statement.
253 + */
254 + private $getrange_stmt;
225 255
226 - /**
227 - * Prepared statement to insert one cache element.
228 - *
229 - * @var SQLite3Stmt
230 - */
231 - private $insertone;
256 + /**
257 + * Prepared statement to delete one cache element.
258 + *
259 + * @var SQLite3Stmt DELETE statement.
260 + */
261 + private $deleteone_stmt;
232 262
233 - /**
234 - * Prepared statement to update one cache element.
235 - *
236 - * @var SQLite3Stmt
237 - */
238 - private $updateone;
263 + /**
264 + * Prepared statement to delete a group of cache elements.
265 + *
266 + * @var SQLite3Stmt
267 + */
268 + private $deletegroup_stmt;
239 269
240 - /**
241 - * Associative array of items we know ARE NOT in SQLite.
242 - *
243 - * @var array Keys are names, values don't matter.
244 - */
245 - private $not_in_persistent_cache = [];
246 - /**
247 - * Associative array of items we know ARE in SQLite.
248 - *
249 - * @var array Keys are names, values don't matter.
250 - */
251 - private $in_persistent_cache = [];
252 - /**
253 - * Cache table name.
254 - *
255 - * @var string Usually 'object_cache'.
256 - */
257 - private $cache_table_name;
258 - /**
259 - * Flag for availability of igbinary serialization extension.
260 - *
261 - * @var bool true if it is available.
262 - */
263 - private $has_igbinary;
264 - /**
265 - * Flag.
266 - *
267 - * @var bool true if hrtime is available.
268 - */
269 - private $has_hrtime;
270 - /**
271 - * Flag.
272 - *
273 - * @var bool true if microtime is available.
274 - */
275 - private $has_microtime;
276 - /**
277 - * The expiration time of non-expiring cache entries has this added to the timestamp.
278 - *
279 - * This is a sentinel value, marking a non-expiring cache entry AND
280 - * recording when it was inserted or updated.
281 - * It allows a least-recently-changed cache-entry purging strategy.
282 - *
283 - * If we wanted a least-recently-used purge, we would need to
284 - * update each cache item's row whenever we accessed it. That
285 - * would cost more than it's worth.
286 - *
287 - * @var int a large number of seconds, much larger than 2**32
288 - */
289 - private $noexpire_timestamp_offset;
290 - /**
291 - * The maximum age of an entry before we get rid of it.
292 - *
293 - * @var int the maximum lifetime of a cache entry, often a week.
294 - */
295 - private $max_lifetime;
296 - /**
297 - * An array of elapsed times for each cache-retrieval operation.
298 - *
299 - * @var array[float]
300 - */
301 - private $select_times = [];
302 - /**
303 - * An array of elapsed times for each cache-insertion / update operation.
304 - *
305 - * @var array[float]
306 - */
307 - private $insert_times = [];
308 - /**
309 - * An array of item names for each cache-retrieval operation.
310 - *
311 - * @var array[string]
312 - */
313 - private $select_names = [];
314 - /**
315 - * An array of item names for each cache-insertion / update operation.
316 - *
317 - * @var array[float]
318 - */
319 - private $insert_names = [];
320 - /**
321 - * An array of elapsed times for each single-row cache deletion operation.
322 - *
323 - * @var array[float]
324 - */
325 - private $delete_times = [];
326 - /**
327 - * The time it took to open the db.
328 - *
329 - * @var float
330 - */
331 - private $open_time;
270 + /**
271 + * Prepared statement to upsert one cache element.
272 + *
273 + * @var SQLite3Stmt
274 + */
275 + private $upsertone_stmt;
332 276
333 - /**
334 - * Monitoring options for the SQLite cache.
335 - *
336 - * Options in array [
337 - * 'capture' => (bool)
338 - * 'resolution' => how often in seconds (float)
339 - * 'lifetime' => how long until entries expire in seconds (int)
340 - * 'verbose' => (bool) capture extra stuff.
341 - * ]
342 - *
343 - * @var array $options Option list.
344 - */
345 - private $monitoring_options;
277 + /**
278 + * Prepared statement to insert one cache element.
279 + *
280 + * @var SQLite3Stmt
281 + */
282 + private $insertone_stmt;
346 283
347 - /**
348 - * Recursion count.
349 - *
350 - * @var int Recursion in the get command.
351 - */
352 - private $get_depth = 31;
353 - /**
354 - * Database object.
355 - * @var SQLite3 instance.
356 - */
357 - private $sqlite;
284 + /**
285 + * Prepared statement to update one cache element.
286 + *
287 + * @var SQLite3Stmt
288 + */
289 + private $updateone_stmt;
358 290
359 - /**
360 - * Constructor for SQLite Object Cache.
361 - *
362 - * @since 2.0.8
363 - */
364 - public function __construct() {
291 + /**
292 + * Prepared statement to clear a flagt.
293 + *
294 + * @var SQLite3Stmt
295 + */
296 + private $clearflag_stmt;
365 297
366 - $this->cache_group_types();
298 + /**
299 + * Prepared statement to set a flagt.
300 + *
301 + * @var SQLite3Stmt
302 + */
303 + private $setflag_stmt;
367 304
368 - $this->has_hrtime = function_exists( 'hrtime' );
369 - $this->has_microtime = function_exists( 'microtime' );
370 - $this->has_igbinary =
371 - function_exists( 'igbinary_serialize' ) && function_exists( 'igbinary_unserialize' );
305 + /**
306 + * Associative array of items we know ARE NOT in SQLite.
307 + *
308 + * When a name is not in this array it means we don't know if it is in SQLite or not.
309 + *
310 + * @var array Keys are cached item names. Values are true.
311 + */
312 + private $not_in_persistent_cache = array();
313 + /**
314 + * Cache table name.
315 + *
316 + * @var string Usually 'object_cache'.
317 + */
318 + private $cache_table_name;
319 + /**
320 + * Flags table name.
321 + *
322 + * @var string Usually 'object_flags'.
323 + */
324 + private $flags_table_name;
325 + /**
326 + * Flag for availability of igbinary serialization extension.
327 + * This will be false if igbinary is not available or if WP_SQLITE_OBJECT_CACHE_SERIALIZE is true.
328 + *
329 + * @var bool true if it is available.
330 + */
331 + private $has_igbinary;
332 + /**
333 + * The expiration time of non-expiring cache entries has this added to the timestamp.
334 + *
335 + * This is a sentinel value, marking a non-expiring cache entry AND
336 + * recording when it was inserted or updated.
337 + * It allows a least-recently-changed cache-entry purging strategy.
338 + *
339 + * If we wanted a least-recently-used purge, we would need to
340 + * update each cache item's row whenever we accessed it. That
341 + * would cost more than it's worth.
342 + *
343 + * @var int a large number of seconds, much larger than 2**32
344 + */
345 + private $noexpire_timestamp_offset;
346 + /**
347 + * The starting time of the request.
348 + * @var
349 + */
350 + private $start_time;
351 + /**
352 + * An array of overall get times, excluding RAM cache.
353 + * @var array
354 + */
355 + private $get_times = array();
356 + /**
357 + * An array of elapsed times for each cache-retrieval operation.
358 + *
359 + * @var array[float]
360 + */
361 + private $select_times = array();
362 + /**
363 + * An array of elapsed times for each cache-insertion / update operation.
364 + *
365 + * @var array[float]
366 + */
367 + private $insert_times = array();
368 + /**
369 + * An array of elapsed times for each single-row cache deletion operation.
370 + *
371 + * @var array[float]
372 + */
373 + private $delete_times = array();
372 374
373 - $this->sqlite_path = $this->create_database_path();
375 + /**
376 + * The times for individual checkpoint -- PRAGMA wal_checkpoint(RESTART) -- times
377 + * @var array
378 + */
379 + private $checkpoint_times = array();
380 + /**
381 + * The times for individual get_multiple operations.
382 + *
383 + * @var array[float]
384 + */
385 + private $get_multiple_times = array();
386 + /**
387 + * The times for apcu_store operations.
388 + * @var array
389 + */
390 + private $apcu_fetch_hit_times = array();
391 + /**
392 + * The times for apcu_store operations.
393 + * @var array
394 + */
395 + private $apcu_fetch_miss_times = array();
396 + /**
397 + * The times for apcu_store operations.
398 + * @var array
399 + */
400 + private $apcu_store_times = array();
374 401
375 - $this->sqlite_timeout = defined( 'WP_SQLITE_OBJECT_CACHE_TIMEOUT' )
376 - ? WP_SQLITE_OBJECT_CACHE_TIMEOUT
377 - : self::SQLITE_TIMEOUT;
402 + /**
403 + * The humber of keys for individual get_multiple operations.
404 + *
405 + * @var array[int]
406 + */
407 + private $get_multiple_keys = array();
408 + /**
409 + * The time it took to open the db.
410 + *
411 + * @var float
412 + */
413 + private $open_time;
378 414
379 - $this->sqlite_journal_mode = defined( 'WP_SQLITE_OBJECT_CACHE_JOURNAL_MODE' )
380 - ? WP_SQLITE_OBJECT_CACHE_JOURNAL_MODE
381 - : self::JOURNAL_MODE;
415 + /**
416 + * Monitoring options for the SQLite cache.
417 + *
418 + * Options in array [
419 + * 'capture' => (bool)
420 + * 'resolution' => how often in seconds (float)
421 + * 'lifetime' => how long until entries expire in seconds (int)
422 + * 'verbose' => (bool) capture extra stuff.
423 + * ]
424 + *
425 + * @var array $options Option list.
426 + */
427 + private $monitoring_options;
382 428
383 - $this->multisite = is_multisite();
384 - $this->blog_prefix = $this->multisite ? get_current_blog_id() . ':' : '';
385 - $this->cache_table_name = self::OBJECT_CACHE_TABLE;
386 - $this->noexpire_timestamp_offset = self::NOEXPIRE_TIMESTAMP_OFFSET;
387 - $this->max_lifetime = self::MAX_LIFETIME;
388 - }
429 + /**
430 + * Recursion count.
431 + *
432 + * @var int Recursion in the get command.
433 + */
434 + private $get_depth = 31;
435 + /**
436 + * Database object.
437 + * @var SQLite3 instance.
438 + */
439 + private $sqlite;
440 + /**
441 + * @var int The max number of digits in optimized integer cache keys.
442 + *
443 + * Longer integers than this are treated as text.
444 + */
445 + private $intkey_length;
446 + /**
447 + * @var int The maximum value of integer keys before we handle them as strings.
448 + *
449 + * Longer integers than this are treated as text.
450 + */
451 + private $intkey_max;
452 + /**
453 + * @var int Erode gaps in consecutive runs of integers by this amount.
454 + *
455 + * This makes for fewer SQL queries at the cost of some extra retrieved items.
456 + */
457 + private $erode_gaps;
389 458
390 - /**
391 - * Create the pathname for the sqlite database.
392 - *
393 - * This is based on WP_SQLITE_OBJECT_CACHE_DB_FILE, WP_CACHE_KEY_SALT,
394 - * and whether igbinary is available.
395 - * It may have -wal and -shm appended to it by the SQLite engine.
396 - *
397 - * @return string Full filesystem pathname for SQLite database.
398 - */
399 - private function create_database_path() {
459 + /**
460 + * @var int mmap_size setting for SQLite. Zero to disable.
461 + */
462 + private $mmap_size = 0;
463 + /**
464 + * The APCu cache is active in this request
465 + * @var bool
466 + */
467 + private $apcu_active = false;
468 + /**
469 + * The APCu cache is active in this site, but not in this request.
470 + *
471 + * This happens for wp-cli programs.
472 + * @var bool
473 + */
474 + private $apcu_supported = false;
475 + private $salt;
476 + /**
477 + * @var string
478 + */
479 + public $apcusalt;
400 480
401 - $result = defined( 'WP_SQLITE_OBJECT_CACHE_DB_FILE' )
402 - ? WP_SQLITE_OBJECT_CACHE_DB_FILE
403 - : WP_CONTENT_DIR . '/' . self::SQLITE_FILENAME;
481 + /**
482 + * Constructor for SQLite Object Cache.
483 + *
484 + * @since 2.0.8
485 + */
486 + public function __construct() {
487 + $this->start_time = hrtime( true );
488 + global $table_prefix;
489 + $this->cache_group_types();
404 490
405 - $salt = defined( 'WP_CACHE_KEY_SALT' )
406 - ? preg_replace( '/[^-_A-Za-z0-9]/', '', WP_CACHE_KEY_SALT )
407 - : '';
408 - $salt .= $this->has_igbinary ? '' : '-a';
491 + /* The environment. */
492 + $apc = defined( 'WP_SQLITE_OBJECT_CACHE_APCU' ) && WP_SQLITE_OBJECT_CACHE_APCU;
493 + $cli = defined( 'WP_CLI' ) && WP_CLI;
494 + $this->apcu_active = $apc && function_exists( 'apcu_enabled' ) && apcu_enabled() && ! $cli;
495 + $this->apcu_supported = $apc && $cli;
409 496
410 - if ( strlen( $salt ) > 0 ) {
411 - $splits = explode( '.', $result );
412 - if ( count( $splits ) >= 2 && 'sqlite' === $splits [ count( $splits ) - 1 ] ) {
413 - $splits[ count( $splits ) - 1 ] = $salt;
414 - $splits [] = 'sqlite';
415 - $result = implode( '.', $splits );
416 - } else {
417 - $result .= '.' . $salt . '.sqlite';
418 - }
419 - }
497 + $force_serialize = defined( 'WP_SQLITE_OBJECT_CACHE_SERIALIZE' ) && WP_SQLITE_OBJECT_CACHE_SERIALIZE;
498 + $this->has_igbinary = function_exists( 'igbinary_serialize' ) && ! $force_serialize;
499 + $this->salt = defined( 'WP_CACHE_KEY_SALT' )
500 + ? preg_replace( '/[^-_A-Za-z0-9]/', '_', WP_CACHE_KEY_SALT )
501 + : '';
502 + if ( $this->apcu_active ) {
503 + /* As unique as possible to avoid collisions with other instances on the same server. */
504 + $this->apcusalt = ( ( '' !== $this->salt )
505 + ? $this->salt
506 + : substr( base64_encode( md5( $this->salt . $table_prefix . DB_HOST . DB_USER . DB_NAME . AUTH_KEY . AUTH_SALT ) ),
507 + 0, 12 ) ) . '|';
420 508
421 - return $result;
422 - }
509 + }
510 + $this->sqlite_path = $this->create_database_path();
423 511
424 - /**
425 - * @param string|null $msg
426 - *
427 - * @return void
428 - */
429 - public static function drop_dead( $msg = null ) {
430 - if ( ! $msg ) {
431 - try {
432 - if ( ! function_exists( '__' ) ) {
433 - wp_load_translations_early();
434 - }
435 - $msg =
436 - __( 'The SQLite Object Cache temporarily failed. Please try again now.', 'sqlite-object-cache' );
437 - } catch ( Exception $ex ) {
438 - /* Can't load translations for some reason */
439 - $msg = 'The SQLite Object Cache temporarily failed. Please try again now.';
440 - }
441 - }
442 - wp_die( esc_html( $msg ) );
443 - }
512 + $this->sqlite_timeout = defined( 'WP_SQLITE_OBJECT_CACHE_TIMEOUT' )
513 + ? WP_SQLITE_OBJECT_CACHE_TIMEOUT
514 + : self::SQLITE_TIMEOUT;
444 515
445 - /**
446 - * Log an error.
447 - *
448 - * @param string $msg
449 - * @param Exception $exception
450 - *
451 - * @return void
452 - */
453 - private function error_log( $msg, $exception = null ) {
454 - $log_exception = ! ! $exception;
455 - $msgs = [];
456 - $msgs [] = 'SQLite Object Cache:';
457 - $msgs [] = $msg;
458 - if ( $this->sqlite ) {
459 - if ( $this->sqlite->lastErrorMsg() ) {
460 - $msgs [] = $this->sqlite->lastErrorMsg();
461 - $msgs [] = '(' . $this->sqlite->lastErrorCode() . ')';
462 - $log_exception = $log_exception && $this->sqlite->lastErrorMsg() !== $exception->getMessage();
463 - }
464 - }
465 - if ( $log_exception ) {
466 - $msgs[] = $exception->getMessage();
467 - $msgs [] = '(' . $exception->getCode() . ')';
468 - $msgs [] = $exception->getTraceAsString();
469 - }
470 - error_log( implode( ' ', $msgs ) );
471 - }
516 + $this->sqlite_journal_mode = defined( 'WP_SQLITE_OBJECT_CACHE_JOURNAL_MODE' )
517 + ? WP_SQLITE_OBJECT_CACHE_JOURNAL_MODE
518 + : self::JOURNAL_MODE;
472 519
473 - /**
474 - * Open SQLite3 connection.
475 - * @return void
476 - */
477 - private function open_connection() {
478 - if ( $this->sqlite ) {
479 - return;
480 - }
481 - $max_retries = 3;
482 - $retries = 0;
483 - while ( ++ $retries <= $max_retries ) {
484 - try {
485 - $this->actual_open_connection();
520 + $this->erode_gaps = defined( 'WP_SQLITE_OBJECT_CACHE_INTKEY_ERODE_GAPS' )
521 + ? (int) WP_SQLITE_OBJECT_CACHE_INTKEY_ERODE_GAPS
522 + : self::INTKEY_ERODE_GAPS;
486 523
487 - return;
488 - } catch ( Exception $ex ) {
489 - /* something went wrong opening */
490 - $this->error_log( 'open_connection failure', $ex );
491 - $this->delete_offending_files( $retries );
492 - }
493 - }
494 - }
524 + $this->intkey_length = defined( 'WP_SQLITE_OBJECT_CACHE_INTKEY_LENGTH' )
525 + ? (int) WP_SQLITE_OBJECT_CACHE_INTKEY_LENGTH
526 + : self::INTKEY_LENGTH;
495 527
496 - /**
497 - * Open SQLite3 connection.
498 - *
499 - * @return void
500 - * @throws Exception Announce SQLite failure.
501 - */
502 - private function actual_open_connection() {
503 - $start = $this->time_usec();
504 - $this->sqlite = new SQLite3( $this->sqlite_path, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, '' );
505 - $this->sqlite->enableExceptions( true );
506 - $this->sqlite->busyTimeout( $this->sqlite_timeout );
528 + $this->intkey_max = - 1 + (int) str_pad( '1', 1 + $this->intkey_length, 0, STR_PAD_RIGHT );
507 529
508 - /* set some initial pragma stuff */
509 - /* NOTE WELL: SQL in this file is not for use with $wpdb, but for SQLite3 */
530 + $this->mmap_size = defined( 'WP_SQLITE_OBJECT_CACHE_MMAP_SIZE' )
531 + ? (int) WP_SQLITE_OBJECT_CACHE_MMAP_SIZE
532 + : self::MMAP_SIZE;
533 + $this->mmap_size = (int) $this->mmap_size * 1024 * 1024;
510 534
511 - /* Notice we sometimes use a journal mode (MEMORY) that risks database corruption.
512 - * That's OK, because it's faster, and because we have an error
513 - * recovery procedure that deletes and recreates a corrupt database file.
514 - */
515 - $this->sqlite->exec( 'PRAGMA synchronous = OFF' );
516 - $this->sqlite->exec( "PRAGMA journal_mode = $this->sqlite_journal_mode" );
517 - $this->sqlite->exec( "PRAGMA encoding = 'UTF-8'" );
518 - $this->sqlite->exec( 'PRAGMA case_sensitive_like = true' );
535 + $this->multisite = is_multisite();
536 + $this->blog_prefix = $this->multisite ? get_current_blog_id() . ':' : '';
537 + $this->cache_table_name = self::OBJECT_CACHE_TABLE;
538 + $this->flags_table_name = self::OBJECT_FLAGS_TABLE;
539 + $this->noexpire_timestamp_offset = self::NOEXPIRE_TIMESTAMP_OFFSET;
540 + $this->open_connection();
519 541
520 - $this->create_object_cache_table();
521 - $this->prepare_statements( $this->cache_table_name );
522 - $this->preload( $this->cache_table_name );
542 + /* If wp-cli code cached something into SQLite, clear the APCu cache because it's stale. */
543 + if ( $this->apcu_active && $this->clear_flag() ) {
544 + $this->apcu_clear_cache();
545 + }
546 + }
523 547
524 - $this->open_time = $this->time_usec() - $start;
525 - }
548 + /**
549 + * Make sure connections are always closed at end of request
550 + */
551 + public function __destruct() {
552 + if ( $this->sqlite ) {
553 + $this->sqlite->close();
554 + unset( $this->sqlite );
555 + }
556 + }
526 557
527 - /**
528 - * Get current time.
529 - *
530 - * @return float Current time in microseconds, from an arbitrary epoch.
531 - */
532 - private function time_usec() {
533 - if ( $this->has_hrtime ) {
534 - /** @noinspection PhpMethodParametersCountMismatchInspection */
535 - /** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */
536 - return hrtime( true ) * 0.001;
537 - }
538 - if ( $this->has_microtime ) {
539 - return microtime( true );
540 - }
558 + /**
559 + * Convert a list of integers into a list of runs: consecutive integers.
560 + *
561 + * Runs expand to include up to $erode_gaps extra integers, to make
562 + * fewer, longer runs. (Each run turns into a single database query,
563 + * so fewer of them is better.)
564 + *
565 + * @param int[] $intkeys List of integers. This can contain duplicate values.
566 + * @param int $erode_gaps Combine runs separated by this or fewer integers.
567 + *
568 + * @return array Associative array with elements start => end
569 + */
570 + private function runs( &$intkeys, $erode_gaps = 2 ) {
571 + if ( 0 === count( $intkeys ) ) {
572 + return array();
573 + }
574 + sort( $intkeys, SORT_NUMERIC );
575 + $previous = $intkeys[0];
576 + $runstart = $previous;
577 + $runs = array();
578 + foreach ( $intkeys as $intkey ) {
579 + if ( $intkey > $previous + 1 + $erode_gaps ) {
580 + $runs[ $runstart ] = $previous;
581 + $runstart = $intkey;
582 + }
583 + $previous = $intkey;
584 + }
585 + if ( null !== $runstart ) {
586 + $runs[ $runstart ] = $previous;
587 + }
541 588
542 - return time() * 1000000.0;
543 - }
589 + return $runs;
590 + }
544 591
545 - /**
546 - * Set group type array
547 - *
548 - * @return void
549 - */
550 - protected function cache_group_types() {
551 - foreach ( $this->global_groups as $group ) {
552 - $this->group_type[ $group ] = 'global';
553 - }
592 + /**
593 + * Create the pathname for the sqlite database.
594 + *
595 + * This is based on WP_SQLITE_OBJECT_CACHE_DB_FILE, WP_CACHE_KEY_SALT,
596 + * and whether igbinary is available.
597 + * It may have -wal and -shm appended to it by the SQLite engine.
598 + *
599 + * @return string Full filesystem pathname for SQLite database.
600 + */
601 + private function create_database_path() {
554 602
555 - foreach ( $this->unflushable_groups as $group ) {
556 - $this->group_type[ $group ] = 'unflushable';
557 - }
603 + $result = defined( 'WP_SQLITE_OBJECT_CACHE_DB_FILE' )
604 + ? WP_SQLITE_OBJECT_CACHE_DB_FILE
605 + : WP_CONTENT_DIR . '/' . self::SQLITE_FILENAME;
558 606
559 - foreach ( $this->ignored_groups as $group ) {
560 - $this->group_type[ $group ] = 'ignored';
561 - }
562 - }
607 + $salt = $this->salt;
608 + $salt .= $this->has_igbinary ? '' : '-a';
563 609
564 - /**
565 - * Do the necessary Data Definition Language work.
566 - *
567 - * @return void
568 - * @throws Exception If something fails.
569 - * @noinspection SqlResolve
570 - */
571 - private function create_object_cache_table() {
572 - /* NOTE WELL: SQL in this file is not for use with $wpdb, but for SQLite3 */
573 - $this->sqlite->exec( 'BEGIN' );
574 - /* does our table exist? */
575 - $q = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND tbl_name = '$this->cache_table_name';";
576 - $r = $this->sqlite->querySingle( $q );
577 - if ( 0 === $r ) {
578 - /* later versions of SQLite3 have clustered primary keys, "WITHOUT ROWID" */
579 - $uses_rowid = version_compare( $this->sqlite_get_version(), '3.8.2' ) < 0;
580 - if ( $uses_rowid ) {
581 - /* @noinspection SqlIdentifier */
582 - $t = "
610 + if ( strlen( $salt ) > 0 ) {
611 + $splits = explode( '.', $result );
612 + if ( count( $splits ) >= 2 && 'sqlite' === $splits [ count( $splits ) - 1 ] ) {
613 + $splits[ count( $splits ) - 1 ] = $salt;
614 + $splits [] = 'sqlite';
615 + $result = implode( '.', $splits );
616 + } else {
617 + $result .= '.' . $salt . '.sqlite';
618 + }
619 + }
620 +
621 + return $result;
622 + }
623 +
624 + /**
625 + * @param string|null $msg
626 + *
627 + * @return void
628 + */
629 + public static function drop_dead( $msg = null ) {
630 + wp_die( $msg ?: 'The SQLite Object Cache temporarily failed. Please try again now.' );
631 + }
632 +
633 + /**
634 + * Log an error.
635 + *
636 + * @param string $msg
637 + * @param Exception $exception
638 + *
639 + * @return void
640 + */
641 + private function error_log( $msg, $exception = null ) {
642 + $log_exception = ! ! $exception;
643 + $msgs = array();
644 + $msgs [] = 'SQLite Object Cache:';
645 + $msgs [] = $this->dropin_version;
646 + $msgs [] = 'SQLite:';
647 + $msgs [] = $this->sqlite_get_version();
648 + $msgs [] = $this->has_igbinary ? 'igbinary' : 'no igbinary';
649 + $msgs [] = $this->apcu_active ? 'APCu active' : 'APCu inactive';
650 + $msgs [] = 'php:';
651 + $msgs [] = PHP_VERSION;
652 + $msgs [] = 'server:';
653 + $msgs [] = $_SERVER['SERVER_SOFTWARE'];
654 + $msgs [] = $msg;
655 + if ( $this->sqlite ) {
656 + if ( $this->sqlite->lastErrorMsg() ) {
657 + $msgs [] = $this->sqlite->lastErrorMsg();
658 + $msgs [] = '(' . $this->sqlite->lastErrorCode() . ')';
659 + $log_exception = $log_exception && $this->sqlite->lastErrorMsg() !== $exception->getMessage();
660 + }
661 + }
662 + if ( $log_exception ) {
663 + $msgs[] = $exception->getMessage();
664 + $msgs [] = '(' . $exception->getCode() . ')';
665 + $msgs [] = $exception->getTraceAsString();
666 + }
667 + error_log( implode( ' ', $msgs ) );
668 + }
669 +
670 + /**
671 + * Open SQLite3 connection.
672 + * @return void
673 + */
674 + private function open_connection() {
675 + if ( $this->sqlite ) {
676 + return;
677 + }
678 + $retries = 3;
679 + while ( $retries -- > 0 ) {
680 + try {
681 + $this->actual_open_connection();
682 +
683 + return;
684 + } catch ( Exception $ex ) {
685 + /* something went wrong opening */
686 + $this->error_log( 'open_connection failure', $ex );
687 + $this->delete_offending_files( $retries );
688 + }
689 + }
690 + }
691 +
692 + /**
693 + * Open SQLite3 connection.
694 + *
695 + * @return void
696 + * @throws Exception Announce SQLite failure.
697 + */
698 + private function actual_open_connection() {
699 + $start = hrtime( true );
700 + $this->sqlite = new SQLite3( $this->sqlite_path, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, '' );
701 + $this->sqlite->enableExceptions( true );
702 + $this->sqlite->busyTimeout( $this->sqlite_timeout );
703 +
704 + /* Set some initial pragma stuff.
705 + * Notice we sometimes use a journal mode (MEMORY) that risks database corruption.
706 + * That's OK, because it's faster, and because we have an error
707 + * recovery procedure that deletes and recreates a corrupt database file.
708 + */
709 + $this->sqlite->exec( 'PRAGMA page_size = 4096' );
710 + if ( $this->mmap_size ) {
711 + $this->sqlite->exec( 'PRAGMA mmap_size = ' . $this->mmap_size );
712 + }
713 + $this->sqlite->exec( 'PRAGMA synchronous = OFF' );
714 + $this->sqlite->exec( "PRAGMA journal_mode = $this->sqlite_journal_mode" );
715 + $this->sqlite->exec( "PRAGMA encoding = 'UTF-8'" );
716 + $this->sqlite->exec( 'PRAGMA case_sensitive_like = true' );
717 + $this->create_object_cache_tables();
718 + $this->prepare_statements( $this->cache_table_name );
719 +
720 + $this->open_time = hrtime( true ) - $start;
721 + }
722 +
723 + /**
724 + * Set group type array
725 + *
726 + * @return void
727 + */
728 + protected function cache_group_types() {
729 + foreach ( $this->global_groups as $group ) {
730 + $this->group_type[ $group ] = 'global';
731 + }
732 +
733 + foreach ( $this->unflushable_groups as $group ) {
734 + $this->group_type[ $group ] = 'unflushable';
735 + }
736 +
737 + foreach ( $this->ignored_groups as $group ) {
738 + $this->group_type[ $group ] = 'ignored';
739 + }
740 + }
741 +
742 + /**
743 + * Do the necessary Data Definition Language work, for the cache table and flags table
744 + *
745 + * We use a single name column comprising group|key in one text string.
746 + * Why?
747 + * In recent versions of SQLite, it can serve as a clustered-index simple primary key.
748 + * SQLite's ANALYZE facilty only builds query - planner stats for the first column of composite keys .
749 + *
750 + * "groups" are all text .
751 + *
752 + * "keys" are sometimes alphanumeric text and sometimes integers . So, they are all treated as text
753 + * in the name column of the database .
754 + *
755 + * Now, range scanning( BETWEEN ) is a hassle in get_multiple, especially when using
756 + * get_multiple to retrieve a range of keys from a group .
757 + *
758 + * @return void
759 + * @throws Exception If something fails .
760 + * @noinspection SqlResolve
761 + */
762 + private function create_object_cache_tables() {
763 + $this->sqlite->exec( 'BEGIN' );
764 + /* does our table exist? */
765 + $q = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND tbl_name = '$this->cache_table_name';";
766 + $r = $this->sqlite->querySingle( $q );
767 + if ( 0 === $r ) {
768 + /* later versions of SQLite3 have clustered primary keys, "WITHOUT ROWID" */
769 + $uses_rowid = version_compare( $this->sqlite_get_version(), '3.8.2' ) < 0;
770 + if ( $uses_rowid ) {
771 + /* @noinspection SqlIdentifier */
772 + $t = "
583 773 CREATE TABLE IF NOT EXISTS $this->cache_table_name (
584 774 name TEXT NOT NULL COLLATE BINARY,
585 - value BLOB,
586 - expires INT
775 + expires INT,
776 + value BLOB
587 777 );
588 - CREATE UNIQUE INDEX IF NOT EXISTS name ON $this->cache_table_name (name);
778 + CREATE UNIQUE INDEX IF NOT EXISTS cache_name ON $this->cache_table_name (name);
589 779 CREATE INDEX IF NOT EXISTS expires ON $this->cache_table_name (expires);";
590 - } else {
591 - /* @noinspection SqlIdentifier */
592 - $t = "
780 + } else {
781 + /* @noinspection SqlIdentifier */
782 + $t = "
593 783 CREATE TABLE IF NOT EXISTS $this->cache_table_name (
594 784 name TEXT NOT NULL PRIMARY KEY COLLATE BINARY,
595 - value BLOB,
596 - expires INT
785 + expires INT,
786 + value BLOB
597 787 ) WITHOUT ROWID;
598 788 CREATE INDEX IF NOT EXISTS expires ON $this->cache_table_name (expires);";
599 - }
789 + }
790 + $this->sqlite->exec( $t );
600 791
601 - $this->sqlite->exec( $t );
602 - }
603 - $this->sqlite->exec( 'COMMIT' );
604 - }
792 + if ( $uses_rowid ) {
793 + /* @noinspection SqlIdentifier */
794 + $t = "
795 + CREATE TABLE IF NOT EXISTS $this->flags_table_name (
796 + name TEXT NOT NULL COLLATE BINARY
797 + );
798 + CREATE UNIQUE INDEX IF NOT EXISTS flags_name ON $this->flags_table_name (name);";
799 + } else {
800 + /* @noinspection SqlIdentifier */
801 + $t = "
802 + CREATE TABLE IF NOT EXISTS $this->flags_table_name (
803 + name TEXT NOT NULL PRIMARY KEY COLLATE BINARY
804 + ) WITHOUT ROWID;";
805 + }
806 + $this->sqlite->exec( $t );
605 807
606 - /**
607 - * Do the necessary Data Definition Language work.
608 - *
609 - * @param string $tbl The name of the table.
610 - *
611 - * @return void
612 - * @throws Exception If something fails.
613 - * @noinspection SqlResolve
614 - */
615 - private function maybe_create_stats_table( $tbl ) {
616 - /* NOTE WELL: SQL in this file is not for use with $wpdb, but for SQLite3 */
617 - $this->sqlite->exec( 'BEGIN' );
618 - /* does our table exist? */
619 - $q = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND tbl_name = '$tbl';";
620 - $r = $this->sqlite->querySingle( $q );
621 - if ( 0 === $r ) {
622 - /* @noinspection SqlIdentifier */
623 - $t = "
808 + /* Put the drop-in's version number in the SQLite file, for troubleshooting. */
809 + $version = str_replace( '.', '0', $this->dropin_version );
810 + if ( is_numeric( $version ) ) {
811 + $this->sqlite->exec( "PRAGMA user_version=" . ( (int) $version ) . ";" );
812 + }
813 + /* Creating SQLite tables; clear APCu at the same time. */
814 + $this->apcu_clear_cache();
815 + }
816 + $this->sqlite->exec( 'COMMIT' );
817 + }
818 +
819 + /**
820 + * Do the necessary Data Definition Language work.
821 + *
822 + * @param string $tbl The name of the table.
823 + *
824 + * @return void
825 + * @throws Exception If something fails.
826 + * @noinspection SqlResolve
827 + */
828 + private function maybe_create_stats_table( $tbl ) {
829 + $this->sqlite->exec( 'BEGIN' );
830 + /* Does our table exist? */
831 + $q = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND tbl_name = '$tbl';";
832 + $r = $this->sqlite->querySingle( $q );
833 + if ( 0 === $r ) {
834 + /* @noinspection SqlIdentifier */
835 + $t = "
624 836 CREATE TABLE IF NOT EXISTS $tbl (
625 837 value BLOB,
626 838 timestamp INT
627 839 );
628 840 CREATE INDEX IF NOT EXISTS expires ON $tbl (timestamp);";
629 - $this->sqlite->exec( $t );
630 - }
631 - $this->sqlite->exec( 'COMMIT' );
632 - }
841 + $this->sqlite->exec( $t );
842 + }
843 + $this->sqlite->exec( 'COMMIT' );
844 + }
633 845
634 - /**
635 - * Create the prepared statements to use.
636 - *
637 - * @param string $tbl Table name.
638 - *
639 - * @return void
640 - * @throws Exception Announce failure.
641 - * @noinspection SqlResolve
642 - */
643 - private function prepare_statements( $tbl ) {
644 - /* NOTE WELL: SQL in this file is not for use with $wpdb, but for SQLite3 */
846 + /**
847 + * Create the prepared statements to use.
848 + *
849 + * @param string $tbl Table name.
850 + *
851 + * @return void
852 + * @throws Exception Announce failure.
853 + * @noinspection SqlResolve
854 + */
855 + private function prepare_statements( $tbl ) {
856 + $now = time();
857 + $this->getone_stmt =
858 + $this->sqlite->prepare( "SELECT value, expires FROM $tbl WHERE name = :name AND expires >= $now;" );
859 + $this->getrange_stmt =
860 + $this->sqlite->prepare( "SELECT name, value, expires FROM $tbl WHERE name BETWEEN :first AND :last AND expires >= $now;" );
861 + $this->deleteone_stmt = $this->sqlite->prepare( "DELETE FROM $tbl WHERE name = :name;" );
862 + $this->deletegroup_stmt = $this->sqlite->prepare( "DELETE FROM $tbl WHERE name LIKE :group || '%';" );
863 + /*
864 + * Some versions of SQLite3 built into php predate the 3.38 advent of unixepoch() (2022-02-22).
865 + * And, others predate the 3.24 advent of UPSERT (that is, ON CONFLICT) syntax.
866 + * In that case we have to do attempt-update then insert to get updates to work. Sigh.
867 + */
868 + $has_upsert = version_compare( $this->sqlite_get_version(), '3.24', 'ge' );
869 + if ( $has_upsert ) {
870 + $this->upsertone_stmt =
871 + $this->sqlite->prepare( "INSERT INTO $tbl (name, value, expires) VALUES (:name, :value, $now + :expires) ON CONFLICT(name) DO UPDATE SET value=excluded.value, expires=excluded.expires;" );
872 + } else {
873 + $this->insertone_stmt =
874 + $this->sqlite->prepare( "INSERT INTO $tbl (name, value, expires) VALUES (:name, :value, $now + :expires);" );
875 + $this->updateone_stmt =
876 + $this->sqlite->prepare( "UPDATE $tbl SET value = :value, expires = $now + :expires WHERE name = :name;" );
877 + }
878 + }
645 879
646 - $now = time();
647 - $this->getone =
648 - $this->sqlite->prepare( "SELECT value FROM $tbl WHERE name = :name AND expires >= $now;" );
649 - $this->deleteone = $this->sqlite->prepare( "DELETE FROM $tbl WHERE name = :name;" );
650 - $this->deletegroup = $this->sqlite->prepare( "DELETE FROM $tbl WHERE name LIKE :group || '.%';" );
651 - /*
652 - * Some versions of SQLite3 built into php predate the 3.38 advent of unixepoch() (2022-02-22).
653 - * And, others predate the 3.24 advent of UPSERT (that is, ON CONFLICT) syntax.
654 - * In that case we have to do attempt-update then insert to get updates to work. Sigh.
655 - */
656 - $has_upsert = version_compare( $this->sqlite_get_version(), '3.24', 'ge' );
657 - if ( $has_upsert ) {
658 - $this->upsertone =
659 - $this->sqlite->prepare( "INSERT INTO $tbl (name, value, expires) VALUES (:name, :value, $now + :expires) ON CONFLICT(name) DO UPDATE SET value=excluded.value, expires=excluded.expires;" );
660 - } else {
661 - $this->insertone =
662 - $this->sqlite->prepare( "INSERT INTO $tbl (name, value, expires) VALUES (:name, :value, $now + :expires);" );
663 - $this->updateone =
664 - $this->sqlite->prepare( "UPDATE $tbl SET value = :value, expires = $now + :expires WHERE name = :name;" );
665 - }
666 - }
880 + /**
881 + * Serialize data for persistence if need be. Use igbinary if available.
882 + *
883 + * @param mixed $data To be serialized.
884 + *
885 + * @return string|mixed Data ready for dbms insertion.
886 + */
887 + private function encode( $data ) {
888 + return $this->has_igbinary
889 + ? igbinary_serialize( $data )
890 + : maybe_serialize( $data );
891 + }
667 892
668 - /**
669 - * Preload frequently accessed items.
670 - *
671 - * @param string $tbl Cache table name.
672 - *
673 - * @return void
674 - * @noinspection SqlResolve
675 - */
676 - public function preload( $tbl ) {
677 - $list =
678 - [
679 - 'options|%',
680 - 'default|%',
681 - 'posts|last_changed',
682 - 'terms|last_changed',
683 - 'site_options|%notoptions',
684 - 'transient|doing_cron',
685 - ];
686 893
687 - $sql = '';
688 - $clauses = [];
689 - foreach ( $list as $item ) {
690 - /* NOTE WELL: SQL in this file is not for use with $wpdb, but for SQLite3 */
691 - $clauses [] = "SELECT name, value FROM $tbl WHERE name LIKE '$item'";
692 - }
693 - $sql .= implode( ' UNION ALL ', $clauses ) . ';';
894 + /**
895 + * Unserialize persistend data. Use igbinary if available.
896 + *
897 + * @param mixed $data To be unserialized.
898 + *
899 + * @return string|mixed Data ready for use.
900 + */
901 + private function decode( $data ) {
902 + return $this->has_igbinary
903 + ? igbinary_unserialize( $data )
904 + : maybe_unserialize( $data );
905 + }
694 906
695 - $resultset = $this->sqlite->query( $sql );
696 - if ( ! $resultset ) {
697 - return;
698 - }
699 - while ( true ) {
700 - $row = $resultset->fetchArray( SQLITE3_NUM );
701 - if ( ! $row ) {
702 - break;
703 - }
704 - list( $group, $key ) = explode( '|', $row[0], 2 );
705 - $val = $this->maybe_unserialize( $row[1] );
706 - /* Put the preloaded value into the cache. */
707 - $this->cache[ $group ][ $key ] = $val;
708 - }
709 - }
907 + /**
908 + * @param mixed $data The serialized data to reconsititute
909 + *
910 + * @return mixed|string The data, cloned if an object.
911 + */
912 + private function reconstitute( $data ) {
913 + $ret = $this->decode( $data );
914 + return is_object( $ret ) ? clone $ret : $ret;
915 + }
710 916
711 - /**
712 - * Serialize data for persistence if need be. Use igbinary if available.
713 - *
714 - * @param mixed $data To be unserialized.
715 - *
716 - * @return string|mixed Data ready for use.
717 - */
718 - private function maybe_unserialize( $data ) {
719 - if ( $this->has_igbinary ) {
720 - return igbinary_unserialize( $data );
721 - }
917 + /**
918 + * Determine whether we can use SQLite3.
919 + *
920 + * @param string $directory The directory to hold the .sqlite file. Default WP_CONTENT_DIR.
921 + *
922 + * @return bool|string true, or an error message.
923 + */
924 + public static function has_sqlite( $directory = WP_CONTENT_DIR ) {
925 + if ( ! wp_is_writable( $directory ) ) {
926 + return sprintf( 'The SQLite Object Cache cannot be activated because the %s directory is not writable.', $directory );
927 + }
722 928
723 - return maybe_unserialize( $data );
724 - }
929 + if ( ! class_exists( 'SQLite3' ) || ! extension_loaded( 'sqlite3' ) ) {
930 + return 'The SQLite Object Cache cannot be activated because the SQLite3 extension is not loaded.';
931 + }
725 932
726 - /**
727 - * Determine whether we can use SQLite3.
728 - *
729 - * @param string $directory The directory to hold the .sqlite file. Default WP_CONTENT_DIR.
730 - *
731 - * @return bool|string true, or an error message.
732 - */
733 - public static function has_sqlite( $directory = WP_CONTENT_DIR ) {
734 - if ( ! wp_is_writable( $directory ) ) {
735 - if ( ! function_exists( '__' ) ) {
736 - wp_load_translations_early();
737 - }
933 + return true;
934 + }
738 935
739 - //TODO THIS goes someplace else
740 - return sprintf( /* translators: 1: WP_CONTENT_DIR */ __( 'The SQLite Object Cache cannot be activated because the %s directory is not writable.', 'sqlite-object-cache' ), $directory );
741 - }
936 + /**
937 + * Set the monitoring options for the SQLite cache.
938 + *
939 + * Options in array [
940 + * 'capture' => (bool)
941 + * 'resolution' => how often in seconds (float)
942 + * 'lifetime' => how long until entries expire in seconds (int)
943 + * 'verbose' => (bool) capture extra stuff.
944 + * ]
945 + *
946 + * @param array $options Option list.
947 + *
948 + * @return void
949 + */
950 + public function set_sqlite_monitoring_options( $options ) {
951 + $this->monitoring_options = $options;
952 + }
742 953
743 - if ( ! class_exists( 'SQLite3' ) || ! extension_loaded( 'sqlite3' ) ) {
744 - if ( ! function_exists( '__' ) ) {
745 - wp_load_translations_early();
746 - }
954 + /**
955 + * Is recording this performance sample appropriate.
956 + *
957 + * We decide to take a performance sample based upon:
958 + * -- the sqlite_object_cache_settings option existing.
959 + * -- $option.capture having the 'on' value.
960 + * -- $option.samplerate >= 100 or samplerate greater than a random number.
961 + *
962 + * @return bool True if this sample should be recorded.
963 + */
964 + private function is_sample() {
965 + $options = get_option( 'sqlite_object_cache_settings', 'missing_option' );
966 + if ( 'missing_option' === $options ) {
967 + /* set an absent option to the empty array, so we don't repeatedly hammer the cache looking for a missing option */
968 + update_option( 'sqlite_object_cache_settings', array(), true );
747 969
748 - return __( 'The SQLite Object Cache cannot be activated because the SQLite3 extension is not loaded.', 'sqlite-object-cache' );
749 - }
970 + return false;
971 + }
972 + if ( is_array( $options ) && array_key_exists( 'capture', $options ) && 'on' === $options['capture'] ) {
973 + if ( array_key_exists( 'samplerate', $options ) && is_numeric( $options['samplerate'] ) ) {
974 + /* samplerate is a percentage likelihood in the option setting */
975 + $samplerate = $options['samplerate'];
976 + if ( $samplerate > 0 ) {
977 + /* a random sample at $samplerate */
978 + if ( $samplerate >= 100 ) {
979 + return true;
980 + }
981 + return ( $samplerate * 10000 ) > rand( 1, 1000000 );
982 + }
983 + }
984 + }
750 985
751 - return true;
752 - }
986 + return false;
987 + }
753 988
754 - /**
755 - * Set the monitoring options for the SQLite cache.
756 - *
757 - * Options in array [
758 - * 'capture' => (bool)
759 - * 'resolution' => how often in seconds (float)
760 - * 'lifetime' => how long until entries expire in seconds (int)
761 - * 'verbose' => (bool) capture extra stuff.
762 - * ]
763 - *
764 - * @param array $options Option list.
765 - *
766 - * @return void
767 - */
768 - public function set_sqlite_monitoring_options( $options ) {
769 - $this->monitoring_options = $options;
770 - }
989 + /**
990 + * Capture statistics if need be. Leave the connection open for late-arriving cache operations.
991 + *
992 + * @return bool
993 + */
994 + public function close() {
995 + if ( $this->sqlite ) {
996 + if ( $this->is_sample() ) {
997 + $this->capture( $this->monitoring_options );
998 + }
999 + /* Once in a while checkpoint the whole WAL log, so it doesn't grow without bound on a busy site. */
1000 + if ( 1 === rand( 1, 5000 ) ) {
1001 + $this->checkpoint();
1002 + }
1003 + }
771 1004
772 - /**
773 - * Is recording this performance sample appropriate.
774 - *
775 - * We decide to take a performance sample based upon:
776 - * -- the sqlite_object_cache_settings option existing.
777 - * -- $option.capture having the 'on' value.
778 - * -- $option.samplerate >= 100 or samplerate greater than a random number.
779 - *
780 - * @return bool True if this sample should be recorded.
781 - */
782 - private function is_sample() {
783 - $options = get_option( 'sqlite_object_cache_settings', 'missing_option' );
784 - if ( 'missing_option' === $options ) {
785 - /* set an absent option to the empty array, so we don't repeatedly hammer the cache looking for a missing option */
786 - update_option( 'sqlite_object_cache_settings', [], true );
1005 + return true;
1006 + }
787 1007
788 - return false;
789 - }
790 - if ( is_array( $options ) && array_key_exists( 'capture', $options ) && 'on' === $options['capture'] ) {
791 - if ( array_key_exists( 'samplerate', $options ) && is_numeric( $options['samplerate'] ) ) {
792 - /* samplerate is a percentage likelihood in the option setting */
793 - $samplerate = $options['samplerate'] * 0.01;
794 - if ( $samplerate > 0.0 ) {
795 - /* a random sample at $samplerate */
796 - if ( $samplerate >= 1.0 ) {
797 - return true;
798 - }
1008 + /**
1009 + * Remove statistics entries from the cache
1010 + *
1011 + * @param int|null $age Number of seconds' worth to retain. Default: retain none.
1012 + *
1013 + * @return void
1014 + */
1015 + public function sqlite_reset_statistics( $age = null ) {
799 1016
800 - return $samplerate >= lcg_value();
801 - }
802 - }
803 - }
1017 + try {
1018 + $object_stats = self::OBJECT_STATS_TABLE;
1019 + $this->maybe_create_stats_table( $object_stats );
1020 + if ( ! is_numeric( $age ) ) {
1021 + /* @noinspection SqlWithoutWhere */
1022 + $sql = "DELETE FROM $object_stats;";
1023 + $this->sqlite->exec( $sql );
1024 + } else {
1025 + $expires = (int) ( time() - $age );
1026 + $limit = self::TRANSACTION_SIZE_LIMIT;
1027 + $hits = $limit;
1028 + while ( $hits >= $limit ) {
1029 + /* @noinspection SqlResolve */
1030 + $sql = "DELETE FROM $object_stats WHERE timestamp IN (SELECT timestamp FROM $object_stats WHERE timestamp < $expires LIMIT $limit);";
1031 + $this->sqlite->exec( $sql );
1032 + $hits = $this->sqlite->changes();
1033 + }
1034 + }
1035 + } catch ( Exception $ex ) {
1036 + $this->error_log( 'SQLite Object Cache exception resetting statistics. ', $ex );
1037 + }
1038 + }
804 1039
805 - return false;
806 - }
1040 + /**
1041 + * Remove old entries.
1042 + *
1043 + * @return boolean True if any items were removed.
1044 + * @noinspection SqlResolve
1045 + */
1046 + public function sqlite_remove_expired() {
1047 + $items_removed = 0;
1048 + try {
1049 + $this->checkpoint();
1050 + $limit = self::TRANSACTION_SIZE_LIMIT;
1051 + $hit = $limit;
807 1052
808 - /**
809 - * Capture statistics if need be, then close the connection.
810 - *
811 - * @return bool
812 - */
813 - public function close() {
814 - $result = true;
815 - if ( $this->sqlite ) {
816 - if ( $this->is_sample() ) {
817 - $this->capture( $this->monitoring_options );
818 - }
819 - $result = $this->sqlite->close();
820 - $this->sqlite = null;
821 - }
1053 + /* Remove items with definite expirations, like transients */
1054 + $sql = 'DELETE FROM ' . $this->cache_table_name . ' WHERE name IN (SELECT name FROM ' . $this->cache_table_name . ' WHERE expires <= ' . time() . ' LIMIT ' . $limit . ')';
822 1055
823 - return $result;
824 - }
1056 + while ( $hit >= $limit ) {
1057 + $this->sqlite->exec( $sql );
1058 + $hit = $this->sqlite->changes();
1059 + $items_removed += $hit;
1060 + }
1061 + } catch ( Exception $ex ) {
1062 + $this->error_log( 'sqlite_remove_expired', $ex );
1063 + }
825 1064
826 - /**
827 - * Generate canonical name for cache item
828 - *
829 - * @param string $key The key name.
830 - * @param string $group The group name.
831 - *
832 - * @return string The name.
833 - */
834 - private function name_from_key_group( $key, $group ) {
835 - return $group . '|' . $key;
836 - }
1065 + return $items_removed > 0;
1066 + }
837 1067
838 - /**
839 - * Serialize data for persistence if need be. Use igbinary if available.
840 - *
841 - * @param mixed $data To be serialized.
842 - *
843 - * @return string|mixed Data ready for dbms insertion.
844 - */
845 - private function maybe_serialize( $data ) {
846 - if ( $this->has_igbinary ) {
847 - return igbinary_serialize( $data );
848 - }
1068 + /**
1069 + * Get the size of the cache database.
1070 + *
1071 + * @return int Size of current cache database in bytes.
1072 + */
1073 + public function sqlite_get_size() {
1074 + $object_cache = self::OBJECT_CACHE_TABLE;
1075 + $sql = "SELECT SUM(LENGTH(value) + LENGTH(name)) length FROM $object_cache";
1076 + $stmt = $this->sqlite->prepare( $sql );
1077 + $resultset = $stmt->execute();
1078 + $row = $resultset->fetchArray( SQLITE3_NUM );
1079 + $result = $row[0];
1080 + $resultset->finalize();
849 1081
850 - return maybe_serialize( $data );
851 - }
1082 + return (int) $result;
1083 + }
852 1084
853 - /**
854 - * Remove statistics entries from the cache
855 - *
856 - * @param int|null $age Number of seconds' worth to retain. Default: retain none.
857 - *
858 - * @return void
859 - */
860 - public function sqlite_reset_statistics( $age = null ) {
861 - try {
862 - if ( ! $this->sqlite ) {
863 - $this->open_connection();
864 - }
865 - $object_stats = self::OBJECT_STATS_TABLE;
866 - $this->maybe_create_stats_table( $object_stats );
867 - /* NOTE WELL: SQL in this file is not for use with $wpdb, but for SQLite3 */
868 - if ( ! is_numeric( $age ) ) {
869 - /* @noinspection SqlWithoutWhere */
870 - $sql = "DELETE FROM $object_stats;";
871 - } else {
872 - $expires = (int) ( time() - $age );
873 - /* @noinspection SqlResolve */
874 - $sql =
875 - "DELETE FROM $object_stats WHERE timestamp < $expires;";
876 - }
877 - $this->sqlite->exec( $sql );
878 - } catch ( Exception $ex ) {
879 - $this->error_log( 'SQLite Object Cache exception resetting statistics. ', $ex );
880 - }
881 - }
1085 + /**
1086 + * Read object names, sizes, expirations from cache, ordered by expiration time oldest first.
1087 + *
1088 + * @param $timestamps true If the timestamps returned should be expirations, false means raw
1089 + *
1090 + * @return Generator of name/length/timestamp rows.
1091 + * @throws Exception Announce SQLite failure.
1092 + * @noinspection SqlResolve
1093 + */
1094 + public function &sqlite_load_usages( $timestamps = true ) {
1095 + $object_cache = self::OBJECT_CACHE_TABLE;
1096 + $offset = $this->noexpire_timestamp_offset;
1097 + $sql = "SELECT name, LENGTH(value) + LENGTH(name) length, expires FROM $object_cache";
1098 + $stmt = $this->sqlite->prepare( $sql );
1099 + try {
1100 + $resultset = $stmt->execute();
1101 + while ( true ) {
1102 + $row = $resultset->fetchArray( SQLITE3_ASSOC );
1103 + if ( ! $row ) {
1104 + break;
1105 + }
1106 + $row = (object) $row;
1107 + if ( $timestamps ) {
1108 + $expires = $row->expires;
1109 + if ( $expires >= self::NOEXPIRE_TIMESTAMP_OFFSET ) {
1110 + $expires -= self::NOEXPIRE_TIMESTAMP_OFFSET;
1111 + }
1112 + $row->expires = $expires;
1113 + }
1114 + yield $row;
1115 + }
1116 + } finally {
1117 + $resultset->finalize();
1118 + }
1119 + }
882 1120
883 - /**
884 - * Remove old entries and VACUUM the database.
885 - *
886 - * @param mixed $retention How long, in seconds, to keep old entries. Default one week.
887 - * @param bool $use_transaction True if the cleanup should be inside BEGIN / COMMIT.
888 - * @param bool $vacuum VACUUM the db.
889 - *
890 - * @return void
891 - * @noinspection SqlResolve
892 - */
893 - public function sqlite_clean_up_cache( $retention = null, $use_transaction = true, $vacuum = false ) {
894 - /* NOTE WELL: SQL in this file is not for use with $wpdb, but for SQLite3 */
895 - try {
896 - if ( ! $this->sqlite ) {
897 - $this->open_connection();
898 - }
899 - if ( $use_transaction ) {
900 - $this->sqlite->exec( 'BEGIN' );
901 - }
902 - /* Remove items with definite expirations, like transients */
903 - $sql = "DELETE FROM $this->cache_table_name WHERE expires <= :now;";
904 - $stmt = $this->sqlite->prepare( $sql );
905 - $stmt->bindValue( ':now', time(), SQLITE3_INTEGER );
906 - $result = $stmt->execute();
907 - $result->finalize();
908 - /* Remove old items. We use the most recent update time. Tracking use time is too expensive. */
909 - $retention = is_numeric( $retention ) ? $retention : $this->max_lifetime;
910 - $sql = "DELETE FROM $this->cache_table_name WHERE expires BETWEEN :offset AND :end;";
911 - $stmt = $this->sqlite->prepare( $sql );
912 - $offset = $this->noexpire_timestamp_offset;
913 - $end = time() + $offset - $retention;
914 - $stmt->bindValue( ':offset', $offset, SQLITE3_INTEGER );
915 - $stmt->bindValue( ':end', $end, SQLITE3_INTEGER );
916 - $result = $stmt->execute();
917 - $result->finalize();
918 - if ( $use_transaction ) {
919 - $this->sqlite->exec( 'COMMIT' );
920 - }
921 - if ( $vacuum ) {
922 - $this->sqlite->exec( 'VACUUM' );
923 - $this->sqlite->exec( 'PRAGMA analysis_limit=400' );
924 - $this->sqlite->exec( 'PRAGMA optimize' );
925 - }
926 - } catch ( Exception $ex ) {
927 - $this->error_log( 'sqlite_clean_up_cache', $ex );
928 - }
929 - }
1121 + public function sqlite_sizes() {
1122 + $object_stats = self::OBJECT_STATS_TABLE;
1123 + $this->maybe_create_stats_table( $object_stats );
930 1124
931 - /**
932 - * Read object names, sizes, expirations from cache.
933 - *
934 - * @param $timestamps true If the timestamps returned should be expirations, false means raw
935 - *
936 - * @return Generator of name/length/timestamp rows.
937 - * @throws Exception Announce SQLite failure.
938 - * @noinspection SqlResolve
939 - */
940 - public function sqlite_load_usages( $timestamps = true ) {
941 - if ( ! $this->sqlite ) {
942 - $this->open_connection();
943 - }
1125 + $items = array(
1126 + 'page_size' => 'PRAGMA page_size;',
1127 + 'free_pages' => 'PRAGMA freelist_count;',
1128 + 'total_pages' => 'PRAGMA page_count;',
1129 + 'stats_items' => "SELECT COUNT(value) FROM $object_stats;",
1130 + 'stats_size' => "SELECT SUM(LENGTH(value)+ 4) FROM $object_stats;",
1131 + 'mmap_size' => "PRAGMA mmap_size;",
1132 + );
944 1133
945 - $object_cache = self::OBJECT_CACHE_TABLE;
946 - $sql = "SELECT name, LENGTH(value) length, expires FROM $object_cache";
947 - $stmt = $this->sqlite->prepare( $sql );
948 - $resultset = $stmt->execute();
949 - while ( true ) {
950 - $row = $resultset->fetchArray( SQLITE3_ASSOC );
951 - if ( ! $row ) {
952 - break;
953 - }
954 - $row = (object) $row;
955 - if ( $timestamps ) {
956 - $expires = $row->expires;
957 - if ( $expires >= self::NOEXPIRE_TIMESTAMP_OFFSET ) {
958 - $expires -= self::NOEXPIRE_TIMESTAMP_OFFSET;
959 - }
960 - $row->expires = $expires;
961 - }
962 - yield $row;
963 - }
964 - $resultset->finalize();
965 - }
1134 + $result = array();
1135 + foreach ( $items as $item => $query ) {
1136 + $stmt = $this->sqlite->prepare( $query );
1137 + $resultset = $stmt->execute();
1138 + $row = $resultset->fetchArray( SQLITE3_NUM );
1139 + $val = (int) $row[0];
1140 + $resultset->finalize();
1141 + $result [ $item ] = $val;
1142 + }
966 1143
967 - /**
968 - * Read rows from the stored statistics.
969 - *
970 - * @return Generator
971 - * @throws Exception Announce SQLite failure.
972 - * @noinspection SqlResolve
973 - */
974 - public function sqlite_load_statistics() {
975 - if ( ! $this->sqlite ) {
976 - $this->open_connection();
977 - }
1144 + return $result;
1145 + }
978 1146
979 - $object_stats = self::OBJECT_STATS_TABLE;
980 - $this->maybe_create_stats_table( $object_stats );
981 - $sql = "SELECT value FROM $object_stats;";
982 - $stmt = $this->sqlite->prepare( $sql );
983 - $resultset = $stmt->execute();
984 - while ( true ) {
985 - $row = $resultset->fetchArray( SQLITE3_NUM );
986 - if ( ! $row ) {
987 - break;
988 - }
989 - $value = $this->maybe_unserialize( $row[0] );
990 - yield (object) $value;
991 - }
992 - $resultset->finalize();
993 - }
1147 + /**
1148 + * Read timestamps and object sizes of non-expiring items, oldest first, in buckets of 16 seconds.
1149 + *
1150 + * Object sizes are the summed lengths of name, value, and timestamp, and ignore index overhead.
1151 + *
1152 + * @return SQLite3Result Resultset containing length/timestamp rows.
1153 + * @throws Exception Announce SQLite failure.
1154 + * @noinspection SqlResolve
1155 + */
1156 + private function sqlite_load_sizes() {
1157 + $object_cache = self::OBJECT_CACHE_TABLE;
1158 + $offset = $this->noexpire_timestamp_offset;
1159 + $sql =
1160 + "SELECT SUM(LENGTH(value) + LENGTH(name) + 6) length, ((expires+15)/16)*16 expires FROM $object_cache WHERE expires >= $offset GROUP BY ((expires+15)/16)*16 ORDER BY ((expires+15)/16)*16";
1161 + $stmt = $this->sqlite->prepare( $sql );
994 1162
995 - /**
996 - * Do the performance-capture operation.
997 - *
998 - * Put a row named sqlite_object_cache.mon.123456 into sqlite containing the raw data.
999 - *
1000 - * @param array $options Contents of $this->monitoring_options.
1001 - *
1002 - * @return void
1003 - * @noinspection SqlResolve
1004 - */
1005 - private function capture( $options ) {
1006 - $now = microtime( true );
1007 - global $wpdb;
1008 - $record = [
1009 - 'time' => $now,
1010 - 'RAMhits' => $this->cache_hits,
1011 - 'RAMmisses' => $this->cache_misses,
1012 - 'DISKhits' => $this->persistent_hits,
1013 - 'DISKmisses' => $this->persistent_misses,
1014 - 'open' => $this->open_time,
1015 - 'selects' => $this->select_times,
1016 - 'inserts' => $this->insert_times,
1017 - 'deletes' => $this->delete_times,
1018 - 'DBMSqueries' => $wpdb->num_queries,
1019 - ];
1020 - if ( is_array( $options ) && $options['verbose'] ) {
1021 - $record ['select_names'] = $this->select_names;
1022 - $record ['delete_names'] = $this->insert_names;
1023 - }
1163 + return $stmt->execute();
1164 + }
1024 1165
1025 - $object_stats = self::OBJECT_STATS_TABLE;
1026 - try {
1027 - if ( ! $this->sqlite ) {
1028 - $this->open_connection();
1029 - }
1030 - $this->maybe_create_stats_table( $object_stats );
1031 - $sql =
1032 - "INSERT INTO $object_stats (value, timestamp) VALUES (:value, :timestamp);";
1033 - $stmt = $this->sqlite->prepare( $sql );
1034 - $stmt->bindValue( ':value', $this->maybe_serialize( $record ), SQLITE3_BLOB );
1035 - $stmt->bindValue( ':timestamp', time(), SQLITE3_INTEGER );
1036 - $result = $stmt->execute();
1037 - $result->finalize();
1038 - } catch ( Exception $ex ) {
1039 - $this->error_log( 'error capturing performance stats, skipping.', $ex );
1040 - }
1041 - unset( $record, $stmt );
1042 - }
1166 + /**
1167 + * Read rows from the stored statistics.
1168 + *
1169 + * @return Generator
1170 + * @throws Exception Announce SQLite failure.
1171 + * @noinspection SqlResolve
1172 + */
1173 + public function sqlite_load_statistics() {
1174 + $object_stats = self::OBJECT_STATS_TABLE;
1175 + $this->maybe_create_stats_table( $object_stats );
1176 + $sql = "SELECT value FROM $object_stats;";
1177 + $stmt = $this->sqlite->prepare( $sql );
1178 + try {
1179 + $resultset = $stmt->execute();
1180 + while ( true ) {
1181 + $row = $resultset->fetchArray( SQLITE3_NUM );
1182 + if ( ! $row ) {
1183 + break;
1184 + }
1185 + $value = $this->decode( $row[0] );
1186 + yield (object) $value;
1187 + }
1188 + } finally {
1189 + $resultset->finalize();
1190 + }
1191 + }
1043 1192
1044 - /**
1045 - * Get the version of SQLite in use.
1046 - *
1047 - * @return string
1048 - */
1049 - public function sqlite_get_version() {
1050 - $v = SQLite3::version();
1193 + /**
1194 + * Do the performance-capture operation.
1195 + *
1196 + * Put a row named sqlite_object_cache.mon.123456 into sqlite containing the raw data.
1197 + *
1198 + * @param array $options Contents of $this->monitoring_options.
1199 + *
1200 + * @return void
1201 + * @noinspection SqlResolve
1202 + */
1203 + private function capture( $options ) {
1204 + $now = microtime( true );
1205 + global $wpdb;
1206 + $record = array(
1207 + 'time' => $now,
1208 + 'elapsed' => hrtime( true ) - $this->start_time,
1209 + 'RAMhits' => $this->cache_hits,
1210 + 'RAMmisses' => $this->cache_misses,
1211 + 'DISKhits' => $this->persistent_hits,
1212 + 'DISKmisses' => $this->persistent_misses,
1213 + 'open' => $this->open_time,
1214 + 'selects' => $this->select_times,
1215 + 'gets' => $this->get_times,
1216 + 'get_multiples' => $this->get_multiple_times,
1217 + 'get_multiple_keys' => $this->get_multiple_keys,
1218 + 'inserts' => $this->insert_times,
1219 + 'deletes' => $this->delete_times,
1220 + 'checkpoints' => $this->checkpoint_times,
1221 + 'DBMSqueries' => $wpdb->num_queries,
1222 + 'RAM' => memory_get_peak_usage( true ),
1223 + 'APCuhits' => $this->apcu_hits,
1224 + 'APCumisses' => $this->apcu_misses,
1225 + 'APCufetchhit' => $this->apcu_fetch_hit_times,
1226 + 'APCufetchmiss' => $this->apcu_fetch_miss_times,
1227 + 'APCustore' => $this->apcu_store_times,
1051 1228
1052 - return $v['versionString'];
1053 - }
1229 + );
1230 + $object_stats = self::OBJECT_STATS_TABLE;
1231 + try {
1232 + $this->maybe_create_stats_table( $object_stats );
1233 + $sql =
1234 + "INSERT INTO $object_stats (value, timestamp) VALUES (:value, :timestamp);";
1235 + $stmt = $this->sqlite->prepare( $sql );
1236 + $stmt->bindValue( ':value', $this->encode( $record ), SQLITE3_BLOB );
1237 + $stmt->bindValue( ':timestamp', time(), SQLITE3_INTEGER );
1238 + $result = $stmt->execute();
1239 + $result->finalize();
1240 + } catch ( Exception $ex ) {
1241 + $this->error_log( 'error capturing performance stats, skipping.', $ex );
1242 + }
1243 + unset( $record, $stmt );
1244 + }
1054 1245
1055 - /**
1056 - * Sets the list of groups not to be cached by Redis.
1057 - *
1058 - * @param array $groups List of groups that are to be ignored.
1059 - */
1060 - public function add_non_persistent_groups( $groups ) {
1061 - /**
1062 - * Filters list of groups to be added to {@see self::$ignored_groups}
1063 - *
1064 - * @param string[] $groups List of groups to be ignored.
1065 - *
1066 - * @since 2.1.7
1067 - */
1068 - $groups = apply_filters( 'sqlite_object_cache_add_non_persistent_groups', (array) $groups );
1246 + /** Get the version of the drop-in.
1247 + *
1248 + * @return string drop-in version.
1249 + */
1250 + public function dropin_get_version() {
1251 + return $this->dropin_version;
1252 + }
1069 1253
1070 - $this->ignored_groups = array_unique( array_merge( $this->ignored_groups, $groups ) );
1071 - $this->cache_group_types();
1072 - }
1254 + /**
1255 + * Get the version of SQLite in use.
1256 + *
1257 + * @return string
1258 + */
1259 + public function sqlite_get_version() {
1260 + if ( $this->sqlite_version ) {
1261 + return $this->sqlite_version;
1262 + }
1263 + $v = SQLite3::version();
1264 + $this->sqlite_version = $v['versionString'];
1073 1265
1074 - /**
1075 - * Makes private properties readable for backward compatibility.
1076 - *
1077 - * @param string $name Property to get.
1078 - *
1079 - * @return mixed Property.
1080 - * @since 4.0.0
1081 - */
1082 - public function __get( $name ) {
1083 - return $this->$name;
1084 - }
1266 + return $this->sqlite_version;
1267 + }
1085 1268
1086 - /**
1087 - * Makes private properties settable for backward compatibility.
1088 - *
1089 - * @param string $name Property to set.
1090 - * @param mixed $value Property value.
1091 - *
1092 - * @return mixed Newly-set property.
1093 - * @since 4.0.0
1094 - */
1095 - public function __set( $name, $value ) {
1096 - return $this->$name = $value;
1097 - }
1269 + /**
1270 + * Sets the list of groups not to be cached by Redis.
1271 + *
1272 + * @param array $groups List of groups that are to be ignored.
1273 + */
1274 + public function add_non_persistent_groups( $groups ) {
1275 + /**
1276 + * Filters list of groups to be added to {@see self::$ignored_groups}
1277 + *
1278 + * @param string[] $groups List of groups to be ignored.
1279 + *
1280 + * @since 2.1.7
1281 + */
1282 + $groups = apply_filters( 'sqlite_object_cache_add_non_persistent_groups', (array) $groups );
1098 1283
1099 - /**
1100 - * Makes private properties checkable for backward compatibility.
1101 - *
1102 - * @param string $name Property to check if set.
1103 - *
1104 - * @return bool Whether the property is set.
1105 - * @since 4.0.0
1106 - */
1107 - public function __isset( $name ) {
1108 - return isset( $this->$name );
1109 - }
1284 + $this->ignored_groups = array_unique( array_merge( $this->ignored_groups, $groups ) );
1285 + $this->cache_group_types();
1286 + }
1110 1287
1111 - /**
1112 - * Makes private properties un-settable for backward compatibility.
1113 - *
1114 - * @param string $name Property to unset.
1115 - *
1116 - * @since 4.0.0
1117 - */
1118 - public function __unset( $name ) {
1119 - unset( $this->$name );
1120 - }
1288 + /**
1289 + * Makes private properties readable for backward compatibility.
1290 + *
1291 + * @param string $name Property to get.
1292 + *
1293 + * @return mixed Property.
1294 + * @since 4.0.0
1295 + */
1296 + public function __get( $name ) {
1297 + return $this->$name;
1298 + }
1121 1299
1122 - /**
1123 - * Adds multiple values to the cache in one call.
1124 - *
1125 - * @param array $data Array of keys and values to be added.
1126 - * @param string $group Optional. Where the cache contents are grouped. Default empty.
1127 - * @param int $expire Optional. When to expire the cache contents, in seconds.
1128 - * Default 0 (no expiration).
1129 - *
1130 - * @return bool[] Array of return values, grouped by key. Each value is either
1131 - * true on success, or false if cache key and group already exist.
1132 - * @since 6.0.0
1133 - */
1134 - public function add_multiple( array $data, $group = '', $expire = 0 ) {
1135 - $values = [];
1136 - try {
1137 - if ( ! $this->sqlite ) {
1138 - $this->open_connection();
1139 - }
1300 + /**
1301 + * Makes private properties settable for backward compatibility.
1302 + *
1303 + * @param string $name Property to set.
1304 + * @param mixed $value Property value.
1305 + *
1306 + * @return mixed Newly-set property.
1307 + * @since 4.0.0
1308 + */
1309 + public function __set( $name, $value ) {
1310 + return $this->$name = $value;
1311 + }
1140 1312
1141 - /* use a transaction to accelerate add_multiple */
1142 - $this->transaction_active = true;
1143 - $this->sqlite->exec( 'BEGIN' );
1144 - foreach ( $data as $key => $value ) {
1145 - $values[ $key ] = $this->add( $key, $value, $group, $expire );
1146 - }
1147 - $this->sqlite->exec( 'COMMIT' );
1148 - $this->transaction_active = false;
1149 - } catch ( Exception $ex ) {
1150 - $this->error_log( 'add_multiple', $ex );
1151 - $this->delete_offending_files();
1152 - self::drop_dead();
1153 - }
1313 + /**
1314 + * Makes private properties checkable for backward compatibility.
1315 + *
1316 + * @param string $name Property to check if set.
1317 + *
1318 + * @return bool Whether the property is set.
1319 + * @since 4.0.0
1320 + */
1321 + public function __isset( $name ) {
1322 + return isset( $this->$name );
1323 + }
1154 1324
1155 - return $values;
1156 - }
1325 + /**
1326 + * Makes private properties un-settable for backward compatibility.
1327 + *
1328 + * @param string $name Property to unset.
1329 + *
1330 + * @since 4.0.0
1331 + */
1332 + public function __unset( $name ) {
1333 + unset( $this->$name );
1334 + }
1157 1335
1158 - /**
1159 - * Adds data to the cache if it doesn't already exist.
1160 - *
1161 - * @param int|string $key What to call the contents in the cache.
1162 - * @param mixed $data The contents to store in the cache.
1163 - * @param string $group Optional. Where to group the cache contents. Default 'default'.
1164 - * @param int $expire Optional. When to expire the cache contents, in seconds.
1165 - * Default 0 (no expiration).
1166 - *
1167 - * @return bool True on success, false if cache key and group already exist.
1168 - * @throws Exception Announce database failure.
1169 - * @since 2.0.0
1170 - *
1171 - * @uses WP_Object_Cache::cache_item_exists() Checks to see if the cache already has data.
1172 - * @uses WP_Object_Cache::set() Sets the data after the checking the cache
1173 - * contents existence.
1174 - */
1175 - public function add( $key, $data, $group = 'default', $expire = 0 ) {
1176 - if ( wp_suspend_cache_addition() ) {
1177 - return false;
1178 - }
1336 + /**
1337 + * Adds multiple values to the cache in one call.
1338 + *
1339 + * @param array $data Array of keys and values to be added.
1340 + * @param string $group Optional. Where the cache contents are grouped. Default empty.
1341 + * @param int $expire Optional. When to expire the cache contents, in seconds.
1342 + * Default 0 (no expiration).
1343 + *
1344 + * @return bool[] Array of return values, grouped by key. Each value is either
1345 + * true on success, or false if cache key and group already exist.
1346 + * @since 6.0.0
1347 + */
1348 + public function add_multiple( array &$data, $group = '', $expire = 0 ) {
1349 + if ( 0 === count( $data ) ) {
1350 + return array();
1351 + }
1352 + $values = array();
1353 + /* sort the array to reduce index page fragmentation */
1354 + ksort( $data, SORT_NUMERIC );
1355 + try {
1356 + /* use a transaction to accelerate add_multiple */
1357 + $this->transaction_active = true;
1358 + $this->sqlite->exec( 'BEGIN' );
1359 + $transaction_size = self::TRANSACTION_SIZE_LIMIT;
1360 + foreach ( $data as $key => $value ) {
1361 + $values[ $key ] = $this->add( $key, $value, $group, $expire );
1362 + /* limit the size of the transaction, hopefully preventing timeouts in other clients */
1363 + if ( -- $transaction_size <= 0 ) {
1364 + $this->sqlite->exec( 'COMMIT' );
1365 + $this->sqlite->exec( 'BEGIN' );
1366 + $transaction_size = self::TRANSACTION_SIZE_LIMIT;
1367 + }
1368 + }
1369 + $this->sqlite->exec( 'COMMIT' );
1370 + $this->transaction_active = false;
1371 + } catch ( Exception $ex ) {
1372 + $this->error_log( 'add_multiple', $ex );
1373 + $this->delete_offending_files();
1374 + self::drop_dead();
1375 + }
1179 1376
1180 - if ( ! $this->is_valid_key( $key ) ) {
1181 - return false;
1182 - }
1377 + return $values;
1378 + }
1183 1379
1184 - if ( empty( $group ) ) {
1185 - $group = 'default';
1186 - }
1380 + /**
1381 + * Adds data to the cache if it doesn't already exist.
1382 + *
1383 + * @param int|string $key What to call the contents in the cache.
1384 + * @param mixed $data The contents to store in the cache.
1385 + * @param string $group Optional. Where to group the cache contents. Default 'default'.
1386 + * @param int $expire Optional. When to expire the cache contents, in seconds.
1387 + * Default 0 (no expiration).
1388 + *
1389 + * @return bool True on success, false if cache key and group already exist.
1390 + * @throws Exception Announce database failure.
1391 + * @since 2.0.0
1392 + *
1393 + * @uses WP_Object_Cache::cache_item_exists() Checks to see if the cache already has data.
1394 + * @uses WP_Object_Cache::set() Sets the data after the checking the cache
1395 + * contents existence.
1396 + */
1397 + public function add( $key, $data, $group = 'default', $expire = 0 ) {
1398 + if ( wp_suspend_cache_addition() ) {
1399 + return false;
1400 + }
1187 1401
1188 - $id = $key;
1189 - if ( $this->multisite && ! isset( $this->global_groups[ $group ] ) ) {
1190 - $id = $this->blog_prefix . $key;
1191 - }
1402 + if ( ! $this->is_valid_key( $key ) ) {
1403 + return false;
1404 + }
1192 1405
1193 - if ( $this->cache_item_exists( $id, $group ) ) {
1194 - return false;
1195 - }
1406 + $name = $this->normalize_name( $key, $group );
1196 1407
1197 - return $this->set( $key, $data, $group, (int) $expire );
1198 - }
1408 + if ( $this->cache_item_not_exists( $name ) ) {
1409 + return $this->set( $key, $data, $group, (int) $expire );
1410 + }
1199 1411
1200 - /**
1201 - * Serves as a utility function to determine whether a key is valid.
1202 - *
1203 - * @param int|string $key Cache key to check for validity.
1204 - *
1205 - * @return bool Whether the key is valid.
1206 - * @since 6.1.0
1207 - */
1208 - protected function is_valid_key( $key ) {
1209 - if ( is_int( $key ) ) {
1210 - return true;
1211 - }
1412 + return false;
1413 + }
1212 1414
1213 - if ( is_string( $key ) && trim( $key ) !== '' ) {
1214 - return true;
1215 - }
1415 + /**
1416 + * Serves as a utility function to determine whether a key is valid.
1417 + *
1418 + * @param int|string $key Cache key to check for validity.
1419 + *
1420 + * @return bool Whether the key is valid.
1421 + * @since 6.1.0
1422 + */
1423 + protected function is_valid_key( $key ) {
1424 + if ( is_int( $key ) ) {
1425 + return true;
1426 + }
1216 1427
1217 - $type = gettype( $key );
1428 + if ( is_string( $key ) && trim( $key ) !== '' ) {
1429 + return true;
1430 + }
1218 1431
1219 - if ( ! function_exists( '__' ) ) {
1220 - wp_load_translations_early();
1221 - }
1432 + $type = gettype( $key );
1222 1433
1223 - $message =
1224 - is_string( $key ) ? __( 'Cache key must not be an empty string.' )
1225 - /* translators: %s: The type of the given cache key. */
1226 - : sprintf( __( 'Cache key must be integer or non-empty string, %s given.' ), $type );
1227 - // phpcs:ignore
1228 - _doing_it_wrong( sprintf( '%s::%s', __CLASS__, debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 2 )[1]['function'] ), $message, '6.1.0' );
1434 + if ( ! function_exists( '__' ) ) {
1435 + wp_load_translations_early();
1436 + }
1229 1437
1230 - return false;
1231 - }
1438 + $message =
1439 + is_string( $key ) ? __( 'Cache key must not be an empty string.' )
1440 + /* translators: %s: The type of the given cache key. */
1441 + : sprintf( __( 'Cache key must be integer or non-empty string, %s given.' ), $type );
1442 + // phpcs:ignore
1443 + _doing_it_wrong( sprintf( '%s::%s', __CLASS__, debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 2 )[1]['function'] ), $message, '6.1.0' );
1232 1444
1233 - /**
1234 - * Determine whether a key exists in the cache.
1235 - *
1236 - * @param int|string $key Cache key to check for existence.
1237 - * @param string $group Cache group for the key existence check.
1238 - *
1239 - * @return bool Whether the key exists in the cache for the given group.
1240 - * @throws Exception Announce database failure.
1241 - * @since 3.4.0
1242 - */
1243 - protected function cache_item_exists( $key, $group ) {
1244 - $exists =
1245 - isset( $this->cache[ $group ] ) && ( isset( $this->cache[ $group ][ $key ] ) || array_key_exists( $key, $this->cache[ $group ] ) );
1246 - if ( ! $exists ) {
1247 - $val = $this->getone( $key, $group );
1248 - if ( null !== $val ) {
1249 - if ( ! array_key_exists( $group, $this->cache ) ) {
1250 - $this->cache [ $group ] = [];
1251 - }
1252 - $this->cache[ $group ][ $key ] = $val;
1253 - $exists = true;
1254 - $this->persistent_hits ++;
1255 - } else {
1256 - $this->persistent_misses ++;
1257 - }
1258 - }
1445 + return false;
1446 + }
1259 1447
1260 - return $exists;
1261 - }
1448 + /**
1449 + * Determine whether a key exists in the cache.
1450 + *
1451 + * As a side-effect and optimization, copy the value from the SQLite store
1452 + * to RAM if it exists in the SQLite store.
1453 + *
1454 + * @param int|string $name Cache key to check for existence.
1455 + *
1456 + * @return bool Whether the key exists in the cache for the given group.
1457 + * @throws Exception Announce database failure.
1458 + * @since 3.4.0
1459 + */
1460 + protected function cache_item_exists( $name ) {
1461 + $exists = false;
1262 1462
1263 - /**
1264 - * Get one item from external cache.
1265 - *
1266 - * @param string $key Cache key.
1267 - * @param string $group Group name.
1268 - *
1269 - * @return mixed|null Cached item, or null if not found. (Cached item can be false.)
1270 - * @throws Exception Announce database failure.
1271 - */
1272 - private function getone( $key, $group ) {
1273 - $start = $this->time_usec();
1274 - $name = $this->name_from_key_group( $key, $group );
1275 - if ( array_key_exists( $name, $this->not_in_persistent_cache ) ) {
1276 - return null;
1277 - }
1278 - $data = null;
1279 - try {
1280 - if ( ! $this->sqlite ) {
1281 - $this->open_connection();
1282 - }
1283 - $stmt = $this->getone;
1284 - $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
1285 - $result = $stmt->execute();
1286 - $row = $result->fetchArray( SQLITE3_NUM );
1287 - $data = false !== $row && is_array( $row ) && 1 === count( $row ) ? $row[0] : null;
1288 - if ( null !== $data ) {
1289 - $data = $this->maybe_unserialize( $data );
1290 - $this->in_persistent_cache [ $name ] = true;
1291 - } else {
1292 - $this->not_in_persistent_cache [ $name ] = true;
1293 - }
1294 - $result->finalize();
1295 - } catch ( Exception $ex ) {
1296 - unset( $this->in_persistent_cache[ $name ] );
1297 - $this->not_in_persistent_cache [ $name ] = true;
1298 - $this->error_log( 'getone', $ex );
1299 - $this->delete_offending_files();
1300 - self::drop_dead();
1301 - }
1463 + if ( array_key_exists( $name, $this->not_in_persistent_cache ) ) {
1464 + return false;
1465 + }
1466 + $val = $this->get_by_name( $name, $fetchsuccess );
1467 + if ( $fetchsuccess ) {
1468 + $this->cache[ $name ] = $val;
1469 + $exists = true;
1470 + $this->persistent_hits ++;
1471 + unset( $this->not_in_persistent_cache[ $name ] );
1472 + } else {
1473 + $this->persistent_misses ++;
1474 + $this->not_in_persistent_cache[ $name ] = true;
1475 + }
1302 1476
1303 - $this->select_times[] = $this->time_usec() - $start;
1304 - $this->select_names[] = $name;
1477 + return $exists;
1478 + }
1305 1479
1306 - return $data;
1307 - }
1480 + /**
1481 + * Determine whether a key does not exist in the cache. either local or SQLite
1482 + *
1483 + * @param int|string $name Cache key to check for existence.
1484 + *
1485 + * @return bool Whether the key does not exists in the cache.
1486 + * @throws Exception Announce database failure.
1487 + * @since 3.4.0
1488 + */
1489 + protected function cache_item_not_exists( $name ) {
1308 1490
1309 - /**
1310 - * Sets the data contents into the cache.
1311 - *
1312 - * The cache contents are grouped by the $group parameter followed by the
1313 - * $key. This allows for duplicate IDs in unique groups. Therefore, naming of
1314 - * the group should be used with care and should follow normal function
1315 - * naming guidelines outside of core WordPress usage.
1316 - *
1317 - * The $expire parameter is not used, because the cache will automatically
1318 - * expire for each time a page is accessed and PHP finishes. The method is
1319 - * more for cache plugins which use files.
1320 - *
1321 - * @param int|string $key What to call the contents in the cache.
1322 - * @param mixed $data The contents to store in the cache.
1323 - * @param string $group Optional. Where to group the cache contents. Default 'default'.
1324 - * @param int $expire Optional. Not used.
1325 - *
1326 - * @return bool True if contents were set, false if key is invalid.
1327 - * @since 2.0.0
1328 - * @since 6.1.0 Returns false if cache key is invalid.
1329 - *
1330 - */
1331 - public function set( $key, $data, $group = 'default', $expire = 0 ) {
1332 - if ( ! $this->is_valid_key( $key ) ) {
1333 - return false;
1334 - }
1491 + if ( array_key_exists( $name, $this->cache ) ) {
1492 + return false;
1493 + }
1494 + if ( array_key_exists( $name, $this->not_in_persistent_cache ) ) {
1495 + return true;
1496 + }
1335 1497
1336 - if ( empty( $group ) ) {
1337 - $group = 'default';
1338 - }
1498 + return ! $this->cache_item_exists( $name );
1499 + }
1339 1500
1340 - if ( $this->multisite && ! isset( $this->global_groups[ $group ] ) ) {
1341 - $key = $this->blog_prefix . $key;
1342 - }
1501 + /**
1502 + * Get one item from external cache.
1503 + *
1504 + * @param string $name Cache key.
1505 + * @param bool|null $success Set to true if the item was found.
1506 + *
1507 + * @return mixed|null Cached item, cloned if an object. Null if not found. (Cached item can be false.)
1508 + */
1509 + private function get_by_name( $name, &$success ) {
1510 + if ( $this->apcu_active ) {
1511 + $astart = hrtime( true );
1512 + $data = apcu_fetch( $this->apcusalt . $name, $fetchsuccess );
1513 + if ( $fetchsuccess ) {
1514 + if ( is_object( $data ) ) {
1515 + $data = clone $data;
1516 + }
1517 + ++ $this->apcu_hits;
1518 + $this->apcu_fetch_hit_times[] = hrtime( true ) - $astart;
1519 + $success = true;
1520 + return $data;
1521 + } else {
1522 + ++ $this->apcu_misses;
1523 + $this->apcu_fetch_miss_times[] = hrtime( true ) - $astart;
1524 + }
1525 + }
1526 + $data = null;
1527 + $fetchsuccess = false;
1528 + $expires = 0;
1529 + $start = hrtime( true );
1530 + try {
1531 + $stmt = $this->getone_stmt;
1532 + $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
1533 + $result = $stmt->execute();
1534 + $row = $result->fetchArray( SQLITE3_NUM );
1535 + if ( false !== $row ) {
1536 + $fetchsuccess = true;
1537 + $expires = $row[1];
1538 + $expires = ( $expires < self::NOEXPIRE_TIMESTAMP_OFFSET ) ? $expires : $expires - self::NOEXPIRE_TIMESTAMP_OFFSET;
1539 + $expires = $expires - time();
1540 + $expires = $expires > 0 ? $expires : DAY_IN_SECONDS;
1541 + $data = $this->reconstitute( $row[0] );
1542 + }
1543 + if ( $fetchsuccess ) {
1544 + /* Pull item into APCu */
1545 + if ( $this->apcu_active ) {
1546 + $astart = hrtime( true );
1547 + apcu_store( $this->apcusalt . $name, $data, $expires );
1548 + $this->apcu_store_times[] = hrtime( true ) - $astart;
1549 + }
1343 1550
1344 - if ( is_object( $data ) ) {
1345 - $data = clone $data;
1346 - }
1551 + unset ( $this->not_in_persistent_cache[ $name ] );
1552 + } else {
1553 + $this->not_in_persistent_cache [ $name ] = true;
1554 + }
1555 + $result->finalize();
1556 + } catch ( Exception $ex ) {
1557 + unset( $this->not_in_persistent_cache [ $name ] );
1558 + $this->error_log( 'get_by_name', $ex );
1559 + $this->delete_offending_files();
1560 + self::drop_dead();
1561 + }
1562 + $this->select_times[] = hrtime( true ) - $start;
1347 1563
1348 - $this->cache[ $group ][ $key ] = $data;
1349 - $this->handle_put( $key, $data, $group, $expire );
1564 + $success = $fetchsuccess;
1565 + return $data;
1566 + }
1350 1567
1351 - return true;
1352 - }
1568 + /**
1569 + * Sets the data contents into the cache.
1570 + *
1571 + * The cache contents are grouped by the $group parameter followed by the
1572 + * $key. This allows for duplicate IDs in unique groups. Therefore, naming of
1573 + * the group should be used with care and should follow normal function
1574 + * naming guidelines outside of core WordPress usage.
1575 + *
1576 + * The $expire parameter is not used, because the cache will automatically
1577 + * expire for each time a page is accessed and PHP finishes. The method is
1578 + * more for cache plugins which use files.
1579 + *
1580 + * @param int|string $key What to call the contents in the cache.
1581 + * @param mixed $data The contents to store in the cache. Objects are cloned before being stored.
1582 + * @param string $group Optional. Where to group the cache contents. Default 'default'.
1583 + * @param int $expire Optional. Not used.
1584 + *
1585 + * @return bool True if contents were set, false if key is invalid.
1586 + * @since 2.0.0
1587 + * @since 6.1.0 Returns false if cache key is invalid.
1588 + *
1589 + */
1590 + public function set( $key, $data, $group = 'default', $expire = 0 ) {
1353 1591
1354 - /**
1355 - * Write to the persistent cache.
1356 - *
1357 - * @param int|string $key What to call the contents in the cache.
1358 - * @param mixed $data The contents to store in the cache.
1359 - * @param string $group Optional. Where to group the cache contents. Default 'default'.
1360 - * @param int $expire Optional. Not used.
1361 - *
1362 - * @return void
1363 - */
1364 - private function handle_put( $key, $data, $group, $expire ) {
1365 - if ( $this->is_ignored_group( $group ) ) {
1366 - return;
1367 - }
1368 - try {
1369 - if ( ! $this->sqlite ) {
1370 - $this->open_connection();
1371 - }
1592 + if ( ! $this->is_valid_key( $key ) ) {
1593 + return false;
1594 + }
1372 1595
1373 - $name = $this->name_from_key_group( $key, $group );
1374 - $start = $this->time_usec();
1375 - $value = $this->maybe_serialize( $data );
1376 - $expires = $expire ?: $this->noexpire_timestamp_offset;
1377 - if ( $this->upsertone ) {
1378 - $stmt = $this->upsertone;
1379 - $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
1380 - $stmt->bindValue( ':value', $value, SQLITE3_BLOB );
1381 - $stmt->bindValue( ':expires', $expires, SQLITE3_INTEGER );
1382 - $result = $stmt->execute();
1383 - $result->finalize();
1384 - } else {
1385 - /* Pre-upsert version (pre- 3.24) of SQLite,
1386 - * Need to try update, then do insert if need be.
1387 - * Race conditions are possible, hence BEGIN / COMMIT
1388 - */
1389 - if ( ! $this->transaction_active ) {
1390 - $this->sqlite->exec( 'BEGIN' );
1391 - }
1392 - $stmt = $this->updateone;
1393 - $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
1394 - $stmt->bindValue( ':value', $value, SQLITE3_BLOB );
1395 - $stmt->bindValue( ':expires', $expires, SQLITE3_INTEGER );
1396 - $result = $stmt->execute();
1397 - $result->finalize();
1398 - if ( 0 === $this->sqlite->changes() ) {
1399 - /* Updated zero rows, so we need an insert. */
1400 - $stmt = $this->insertone;
1401 - $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
1402 - $stmt->bindValue( ':value', $value, SQLITE3_BLOB );
1403 - $stmt->bindValue( ':expires', $expires, SQLITE3_INTEGER );
1404 - $result = $stmt->execute();
1405 - $result->finalize();
1406 - }
1407 - if ( ! $this->transaction_active ) {
1408 - $this->sqlite->exec( 'COMMIT' );
1409 - }
1410 - }
1411 - } catch ( Exception $ex ) {
1412 - $this->error_log( 'handle_put', $ex );
1413 - $this->delete_offending_files();
1414 - self::drop_dead();
1415 - }
1416 - unset( $this->not_in_persistent_cache[ $name ] );
1417 - $this->in_persistent_cache[ $name ] = true;
1418 - /* track how long it took. */
1419 - $this->insert_times[] = $this->time_usec() - $start;
1420 - $this->insert_names[] = $name;
1421 - }
1596 + $name = $this->normalize_name( $key, $group );
1422 1597
1423 - /**
1424 - * Replaces the contents in the cache, if contents already exist.
1425 - *
1426 - * @param int|string $key What to call the contents in the cache.
1427 - * @param mixed $data The contents to store in the cache.
1428 - * @param string $group Optional. Where to group the cache contents. Default 'default'.
1429 - * @param int $expire Optional. When to expire the cache contents, in seconds.
1430 - * Default 0 (no expiration).
1431 - *
1432 - * @return bool True if contents were replaced, false if original value does not exist.
1433 - * @see WP_Object_Cache::set()
1434 - *
1435 - * @since 2.0.0
1436 - *
1437 - */
1438 - public function replace( $key, $data, $group = 'default', $expire = 0 ) {
1439 - if ( ! $this->is_valid_key( $key ) ) {
1440 - return false;
1441 - }
1598 + if ( is_object( $data ) ) {
1599 + $data = clone $data;
1600 + }
1442 1601
1443 - if ( empty( $group ) ) {
1444 - $group = 'default';
1445 - }
1602 + $this->cache[ $name ] = $data;
1446 1603
1447 - $id = $key;
1448 - if ( $this->multisite && ! isset( $this->global_groups[ $group ] ) ) {
1449 - $id = $this->blog_prefix . $key;
1450 - }
1604 + if ( $this->is_ignored_group( $group ) ) {
1605 + return true;
1606 + }
1451 1607
1452 - if ( ! $this->cache_item_exists( $id, $group ) ) {
1453 - return false;
1454 - }
1608 + $start = hrtime( true );
1609 + $this->put_by_name( $name, $data, $expire );
1610 + $this->insert_times[] = hrtime( true ) - $start;
1455 1611
1456 - return $this->set( $key, $data, $group, (int) $expire );
1457 - }
1612 + return true;
1613 + }
1458 1614
1459 - /**
1460 - * Sets multiple values to the cache in one call.
1461 - *
1462 - * @param array $data Array of key and value to be set.
1463 - * @param string $group Optional. Where the cache contents are grouped. Default empty.
1464 - * @param int $expire Optional. When to expire the cache contents, in seconds.
1465 - * Default 0 (no expiration).
1466 - *
1467 - * @return bool[] Array of return values, grouped by key. Each value is always true.
1468 - * @since 6.0.0
1469 - */
1470 - public function set_multiple( array $data, $group = '', $expire = 0 ) {
1471 - $values = [];
1472 - try {
1473 - if ( ! $this->sqlite ) {
1474 - $this->open_connection();
1475 - }
1615 + /**
1616 + * Write to the persistent cache, with timeout retry.
1617 + *
1618 + * @param string $name What to call the contents in the cache.
1619 + * @param mixed $data The contents to store in the cache.
1620 + * @param int $expire Optional.
1621 + *
1622 + * @return void
1623 + */
1624 + private function put_by_name( $name, $data, $expire ) {
1625 + $exception = null;
1626 + $expires = $expire ?: $this->noexpire_timestamp_offset;
1627 + $retries = 3;
1628 + while ( $retries -- > 0 ) {
1629 + try {
1630 + $this->actual_put_by_name( $name, $this->encode( $data ), $expires );
1631 + unset( $this->not_in_persistent_cache[ $name ] );
1632 + if ( $this->apcu_active ) {
1633 + $astart = hrtime( true );
1634 + apcu_store( $this->apcusalt . $name, $data, $expire ?: DAY_IN_SECONDS );
1635 + $this->apcu_store_times[] = hrtime( true ) - $astart;
1636 + }
1637 + return;
1638 + } catch ( Exception $ex ) {
1639 + $exception = $ex;
1640 + if ( ! str_contains( $ex->getMessage(), 'database is locked' ) || 5 !== $this->sqlite->lastErrorCode() ) {
1641 + break;
1642 + }
1643 + }
1644 + sleep( 1 );
1645 + }
1646 + if ( $exception ) {
1647 + $this->error_log( 'put_by_name', $exception );
1648 + $this->delete_offending_files();
1649 + self::drop_dead();
1650 + }
1651 + }
1476 1652
1477 - /* use a transaction to accelerate set_multiple */
1478 - $this->transaction_active = true;
1479 - $this->sqlite->exec( 'BEGIN' );
1653 + /**
1654 + * Actually write to the cache.
1655 + *
1656 + * @param string $name What to call the contents in the cache.
1657 + * @param string $value The seriolized value.
1658 + * @param int $expires Expiration time.
1659 + *
1660 + * @return void
1661 + */
1662 + private function actual_put_by_name( $name, $value, $expires ) {
1663 + if ( $this->apcu_supported ) {
1664 + $this->set_flag();
1665 + }
1666 + if ( $this->upsertone_stmt ) {
1667 + $stmt = $this->upsertone_stmt;
1668 + $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
1669 + $stmt->bindValue( ':value', $value, SQLITE3_BLOB );
1670 + $stmt->bindValue( ':expires', $expires, SQLITE3_INTEGER );
1671 + $result = $stmt->execute();
1672 + $result->finalize();
1673 + } else {
1674 + /* Pre-upsert version (pre- 3.24) of SQLite,
1675 + * Need to try update, then do insert if need be.
1676 + * Race conditions are possible, hence BEGIN / COMMIT
1677 + */
1678 + if ( ! $this->transaction_active ) {
1679 + $this->sqlite->exec( 'BEGIN' );
1680 + }
1681 + $stmt = $this->updateone_stmt;
1682 + $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
1683 + $stmt->bindValue( ':value', $value, SQLITE3_BLOB );
1684 + $stmt->bindValue( ':expires', $expires, SQLITE3_INTEGER );
1685 + $result = $stmt->execute();
1686 + $result->finalize();
1687 + if ( 0 === $this->sqlite->changes() ) {
1688 + /* Updated zero rows, so we need an insert. */
1689 + $stmt = $this->insertone_stmt;
1690 + $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
1691 + $stmt->bindValue( ':value', $value, SQLITE3_BLOB );
1692 + $stmt->bindValue( ':expires', $expires, SQLITE3_INTEGER );
1693 + $result = $stmt->execute();
1694 + $result->finalize();
1480 1695
1481 - foreach ( $data as $key => $value ) {
1482 - $values[ $key ] = $this->set( $key, $value, $group, $expire );
1483 - }
1484 - $this->sqlite->exec( 'COMMIT' );
1485 - $this->transaction_active = false;
1486 - } catch ( Exception $ex ) {
1487 - $this->error_log( 'set_multiple', $ex );
1488 - $this->delete_offending_files();
1489 - self::drop_dead();
1490 - }
1696 + }
1697 + if ( ! $this->transaction_active ) {
1698 + $this->sqlite->exec( 'COMMIT' );
1699 + }
1700 + }
1701 + }
1491 1702
1492 - return $values;
1493 - }
1703 + /**
1704 + * Replaces the contents in the cache, if contents already exist.
1705 + *
1706 + * @param int|string $key What to call the contents in the cache.
1707 + * @param mixed $data The contents to store in the cache.
1708 + * @param string $group Optional. Where to group the cache contents. Default 'default'.
1709 + * @param int $expire Optional. When to expire the cache contents, in seconds.
1710 + * Default 0 (no expiration).
1711 + *
1712 + * @return bool True if contents were replaced, false if original value does not exist.
1713 + * @see WP_Object_Cache::set()
1714 + *
1715 + * @since 2.0.0
1716 + *
1717 + */
1718 + public function replace( $key, $data, $group = 'default', $expire = 0 ) {
1719 + if ( ! $this->is_valid_key( $key ) ) {
1720 + return false;
1721 + }
1494 1722
1495 - /**
1496 - * Retrieves multiple values from the cache in one call.
1497 - *
1498 - * @param array $keys Array of keys under which the cache contents are stored.
1499 - * @param string $group Optional. Where the cache contents are grouped. Default 'default'.
1500 - * @param bool $force Optional. Whether to force an update of the local cache
1501 - * from the persistent cache. Default false.
1502 - *
1503 - * @return array Array of return values, grouped by key. Each value is either
1504 - * the cache contents on success, or false on failure.
1505 - * @since 5.5.5
1506 - */
1507 - public function get_multiple( $keys, $group = 'default', $force = false ) {
1508 - $values = [];
1509 - try {
1510 - if ( ! $this->sqlite ) {
1511 - $this->open_connection();
1512 - }
1723 + $name = $this->normalize_name( $key, $data );
1513 1724
1514 - /* use a transaction to accelerate get_multiple */
1515 - $this->transaction_active = true;
1516 - $this->sqlite->exec( 'BEGIN' );
1725 + if ( $this->cache_item_not_exists( $name ) ) {
1726 + return false;
1727 + }
1517 1728
1518 - foreach ( $keys as $key ) {
1519 - $values[ $key ] = $this->get( $key, $group, $force );
1520 - }
1521 - $this->sqlite->exec( 'COMMIT' );
1522 - $this->transaction_active = false;
1523 - } catch ( Exception $ex ) {
1524 - $this->error_log( 'get_multiple', $ex );
1525 - $this->delete_offending_files();
1526 - self::drop_dead();
1527 - }
1729 + return $this->set( $key, $data, $group, (int) $expire );
1730 + }
1528 1731
1529 - return $values;
1530 - }
1732 + /**
1733 + * Sets multiple values to the cache in one call.
1734 + *
1735 + * @param array $data Array of key and value to be set.
1736 + * @param string $group Optional. Where the cache contents are grouped. Default empty.
1737 + * @param int $expire Optional. When to expire the cache contents, in seconds.
1738 + * Default 0 (no expiration).
1739 + *
1740 + * @return bool[] Array of return values, grouped by key. Each value is always true.
1741 + * @since 6.0.0
1742 + */
1743 + public function set_multiple( array &$data, $group = '', $expire = 0 ) {
1744 + if ( 0 === count( $data ) ) {
1745 + return array();
1746 + }
1747 + $values = array();
1748 + /* Sort the array to reduce index page fragmentation */
1749 + ksort( $data, SORT_NUMERIC );
1750 + try {
1751 + /* use a transaction to accelerate set_multiple */
1752 + $this->transaction_active = true;
1753 + $this->sqlite->exec( 'BEGIN' );
1754 + $transaction_size = self::TRANSACTION_SIZE_LIMIT;
1531 1755
1532 - /**
1533 - * Retrieves the cache contents, if it exists.
1534 - *
1535 - * The contents will be first attempted to be retrieved by searching by the
1536 - * key in the cache group. If the cache is hit (success) then the contents
1537 - * are returned.
1538 - *
1539 - * On failure, the number of cache misses will be incremented.
1540 - *
1541 - * @param int|string $key The key under which the cache contents are stored.
1542 - * @param string $group Optional. Where the cache contents are grouped. Default 'default'.
1543 - * @param bool $force Optional. Whether to force an update of the local cache
1544 - * from the persistent cache. Default false.
1545 - * @param bool $found Optional. Whether the key was found in the cache (passed by reference).
1546 - * Disambiguates a return of false, a storable value. Default null.
1547 - *
1548 - * @return mixed|false The cache contents on success, false on failure to retrieve contents.
1549 - * @since 2.0.0
1550 - */
1551 - public function get( $key, $group = 'default', $force = false, &$found = null ) {
1552 - if ( -- $this->get_depth <= 0 ) {
1553 - return false;
1554 - }
1756 + foreach ( $data as $key => $value ) {
1757 + $values[ $key ] = $this->set( $key, $value, $group, $expire );
1758 + /* limit the size of the transaction, hopefully preventing timeouts in other clients */
1759 + if ( -- $transaction_size <= 0 ) {
1760 + $this->sqlite->exec( 'COMMIT' );
1761 + $this->sqlite->exec( 'BEGIN' );
1762 + $transaction_size = self::TRANSACTION_SIZE_LIMIT;
1763 + }
1764 + }
1765 + $this->sqlite->exec( 'COMMIT' );
1766 + $this->transaction_active = false;
1767 + } catch ( Exception $ex ) {
1768 + $this->error_log( 'set_multiple', $ex );
1769 + $this->delete_offending_files();
1770 + self::drop_dead();
1771 + }
1555 1772
1556 - if ( ! $this->is_valid_key( $key ) ) {
1557 - ++ $this->get_depth;
1773 + return $values;
1774 + }
1558 1775
1559 - return false;
1560 - }
1776 + /**
1777 + * Retrieves multiple values from the cache in one call.
1778 + *
1779 + * @param string[]|int[] $input_keys
1780 + * @param string $group Optional. Where the cache contents are grouped. Default 'default'.
1781 + * @param bool $force Optional. Whether to force an update of the local cache
1782 + * from the persistent cache. Default false.
1783 + *
1784 + * @return array Array of return values, grouped by key. Each value is either
1785 + * the cache contents on success, or false on failure. Objects are cloned
1786 + * before putting them in the array.
1787 + * @since 5.5.5
1788 + */
1789 + public function get_multiple( $input_keys, $group = 'default', $force = false ) {
1790 + $values = array();
1791 + if ( count( $input_keys ) <= 1 || $force ) {
1792 + /* Send the degenerate get_multiple calls, and forced calls, to plain old get. That logic is simpler. */
1793 + foreach ( $input_keys as $key ) {
1794 + $values[ $key ] = $this->get( $key, $group, $force );
1795 + }
1561 1796
1562 - if ( empty( $group ) ) {
1563 - $group = 'default';
1564 - }
1797 + return $values;
1798 + }
1799 + $start = hrtime( true );
1565 1800
1566 - if ( $this->multisite && ! isset( $this->global_groups[ $group ] ) ) {
1567 - $key = $this->blog_prefix . $key;
1568 - }
1801 + $normalized = array();
1802 + $keys_not_found = array();
1803 + /* Find already-cached keys, pruning down the list of keys to fetch. */
1804 + foreach ( $input_keys as $key ) {
1805 + $name = $this->normalize_name( $key, $group );
1806 + $normalized [ $key ] = $name;
1807 + if ( array_key_exists( $name, $this->cache ) ) {
1808 + $values [ $key ] = is_object( $this->cache[ $name ] )
1809 + ? clone $this->cache[ $name ]
1810 + : $this->cache[ $name ];
1811 + ++ $this->cache_hits;
1812 + } else {
1813 + $keys_not_found[ $key ] = $name;
1814 + $values[ $key ] = false;
1815 + }
1816 + }
1569 1817
1570 - if ( $force ) {
1571 - unset( $this->cache[ $group ][ $key ] );
1572 - }
1818 + /* Examine APCu cache for stashed items. */
1819 + if ( $this->apcu_active && count( $keys_not_found ) > 0 ) {
1820 + $keys_not_found_apcu = array();
1821 + foreach ( $keys_not_found as $key => $name ) {
1822 + //TODO this can get an array form of the fetch operation.
1823 + $astart = hrtime( true );
1824 + $val = apcu_fetch( $this->apcusalt . $name, $success );
1825 + if ( $success ) {
1826 + if ( is_object( $val ) ) {
1827 + $val = clone $val;
1828 + }
1829 + ++ $this->apcu_hits;
1830 + $this->apcu_fetch_hit_times[] = hrtime( true ) - $astart;
1573 1831
1574 - try {
1575 - if ( $this->cache_item_exists( $key, $group ) ) {
1576 - $found = true;
1577 - ++ $this->cache_hits;
1578 - if ( is_object( $this->cache[ $group ][ $key ] ) ) {
1579 - ++ $this->get_depth;
1832 + $values [ $key ] = $val;
1833 + } else {
1834 + ++ $this->apcu_misses;
1835 + $this->apcu_fetch_miss_times[] = hrtime( true ) - $astart;
1836 + $keys_not_found_apcu[ $key ] = $name;
1837 + }
1838 + }
1839 + $keys_not_found = $keys_not_found_apcu;
1840 + }
1580 1841
1581 - return clone $this->cache[ $group ][ $key ];
1582 - }
1583 - ++ $this->get_depth;
1842 + if ( count( $keys_not_found ) <= 1 ) {
1843 + /* Degenerate case after fulfilment from RAM: handle as simple get */
1844 + foreach ( $keys_not_found as $key => $name ) {
1845 + $success = false;
1846 + $data = $this->get_by_normalized_name( $name, $success );
1847 + if ( $success ) {
1848 + $values[ $key ] = $data;
1849 + }
1850 + }
1584 1851
1585 - return $this->cache[ $group ][ $key ];
1586 - }
1587 - } catch ( Exception $ex ) {
1588 - $this->delete_offending_files();
1852 + $this->get_multiple_times[] = hrtime( true ) - $start;
1853 + $this->get_multiple_keys [] = count( $input_keys );
1854 + return $values;
1855 + }
1856 + /* split into alpha and numeric keys */
1857 + $alphakeys = array();
1858 + $intkeys = array();
1859 + foreach ( $keys_not_found as $key => $name ) {
1860 + if ( is_numeric( $key ) && (int) $key == $key && (int) $key > 0 && (int) $key <= $this->intkey_max ) {
1861 + $intkeys [] = (int) $key;
1862 + } else {
1863 + $alphakeys [ $key ] = $name;
1864 + }
1865 + }
1866 + try {
1867 + /* Get the consecutive integer key runs */
1868 + $runs = $this->runs( $intkeys, $this->erode_gaps );
1589 1869
1590 - ++ $this->get_depth;
1870 + /* use a transaction to accelerate get_multiple */
1871 + $this->transaction_active = true;
1872 + $this->sqlite->exec( 'BEGIN' );
1873 + $transaction_size = self::TRANSACTION_SIZE_LIMIT;
1591 1874
1592 - return false;
1593 - }
1875 + /* Start by loading the consecutive runs of int keys */
1876 + foreach ( $runs as $first => $last ) {
1877 + $stmt = $this->getrange_stmt;
1878 + $stmt->bindValue( ':first', $normalized[ $first ], SQLITE3_TEXT );
1879 + $stmt->bindValue( ':last', $normalized[ $last ], SQLITE3_TEXT );
1880 + $resultset = $stmt->execute();
1881 + while ( true ) {
1882 + $row = $resultset->fetchArray( SQLITE3_NUM );
1883 + if ( ! $row ) {
1884 + break;
1885 + }
1886 + ++ $this->persistent_hits;
1887 + $name = $row[0];
1888 + $this->cache[ $name ] = $this->reconstitute( $row[1] );
1594 1889
1595 - $found = false;
1596 - $this->cache_misses ++;
1890 + $expires = $row[2];
1891 + $expires = ( $expires < self::NOEXPIRE_TIMESTAMP_OFFSET ) ? $expires : $expires - self::NOEXPIRE_TIMESTAMP_OFFSET;
1892 + $expires = $expires - time();
1893 + $expires = $expires > 0 ? $expires : DAY_IN_SECONDS;
1597 1894
1598 - ++ $this->get_depth;
1895 + if ( $this->apcu_active ) {
1896 + $astart = hrtime( true );
1897 + apcu_store( $this->apcusalt . $name, $this->cache[ $name ], $expires );
1898 + $this->apcu_store_times[] = hrtime( true ) - $astart;
1599 1899
1600 - return false;
1601 - }
1900 + }
1901 + unset( $this->not_in_persistent_cache[ $name ] );
1902 + }
1903 + $resultset->finalize();
1904 + /* limit the size of the transaction, hopefully preventing timeouts in other clients */
1905 + if ( -- $transaction_size <= 0 ) {
1906 + $this->sqlite->exec( 'COMMIT' );
1907 + $this->sqlite->exec( 'BEGIN' );
1908 + $transaction_size = self::TRANSACTION_SIZE_LIMIT;
1909 + }
1910 + }
1911 + /* Do the alpha keys, if any */
1912 + foreach ( $alphakeys as $key => $name ) {
1913 + if ( false === $values[ $key ] ) {
1914 + $success = false;
1915 + $data = $this->get_by_normalized_name( $name, $success );
1916 + if ( $success ) {
1917 + $values[ $key ] = $data;
1918 + }
1919 + /* limit the size of the transaction, hopefully preventing timeouts in other clients */
1920 + if ( -- $transaction_size <= 0 ) {
1921 + $this->sqlite->exec( 'COMMIT' );
1922 + $this->sqlite->exec( 'BEGIN' );
1923 + $transaction_size = self::TRANSACTION_SIZE_LIMIT;
1924 + }
1925 + }
1926 + }
1927 + foreach ( $intkeys as $key ) {
1928 + if ( false === $values[ $key ] ) {
1929 + $success = false;
1930 + $data = $this->get_by_normalized_name( $normalized[ $key ], $success );
1931 + if ( $success ) {
1932 + $values[ $key ] = $data;
1933 + }
1934 + /* limit the size of the transaction, hopefully preventing timeouts in other clients */
1935 + if ( -- $transaction_size <= 0 ) {
1936 + $this->sqlite->exec( 'COMMIT' );
1937 + $this->sqlite->exec( 'BEGIN' );
1938 + $transaction_size = self::TRANSACTION_SIZE_LIMIT;
1939 + }
1940 + }
1941 + }
1942 + $this->sqlite->exec( 'COMMIT' );
1943 + $this->transaction_active = false;
1944 + } catch ( Exception $ex ) {
1945 + $this->error_log( 'get_multiple', $ex );
1946 + $this->delete_offending_files();
1947 + self::drop_dead();
1948 + }
1949 + $this->get_multiple_keys [] = count( $input_keys );
1950 + $this->get_multiple_times [] = hrtime( true ) - $start;
1602 1951
1603 - /**
1604 - * Deletes multiple values from the cache in one call.
1605 - *
1606 - * @param array $keys Array of keys to be deleted.
1607 - * @param string $group Optional. Where the cache contents are grouped. Default empty.
1608 - *
1609 - * @return bool[] Array of return values, grouped by key. Each value is either
1610 - * true on success, or false if the contents were not deleted.
1611 - * @since 6.0.0
1612 - */
1613 - public function delete_multiple( array $keys, $group = '' ) {
1614 - $values = [];
1952 + return $values;
1953 + }
1615 1954
1616 - foreach ( $keys as $key ) {
1617 - $values[ $key ] = $this->delete( $key, $group );
1618 - }
1955 + /**
1956 + * Get the cache row name for a key and group.
1957 + *
1958 + * Notice that numeric keys have leading zeros applied so we can do range queries.
1959 + *
1960 + * @param int|string $key Key name.
1961 + * @param string $group Group name, default = 'default'.
1962 + *
1963 + * @return string
1964 + */
1965 + private function normalize_name( $key, $group ) {
1966 + if ( is_numeric( $key ) && (int) $key == $key && (int) $key >= 0 && (int) $key <= $this->intkey_max ) {
1967 + $key = self::INTKEY_SENTINEL . str_pad( $key, 1 + $this->intkey_length, '0', STR_PAD_LEFT );
1968 + }
1619 1969
1620 - return $values;
1621 - }
1970 + if ( $this->multisite && ! isset( $this->global_groups[ $group ] ) ) {
1971 + $key = $this->blog_prefix . $key;
1972 + }
1973 + if ( empty( $group ) ) {
1974 + $group = 'default';
1975 + }
1622 1976
1623 - /**
1624 - * Removes the contents of the cache key in the group.
1625 - *
1626 - * If the cache key does not exist in the group, then nothing will happen.
1627 - *
1628 - * @param int|string $key What the contents in the cache are called.
1629 - * @param string $group Optional. Where the cache contents are grouped. Default 'default'.
1630 - * @param bool $deprecated Optional. Unused. Default false.
1631 - *
1632 - * @return bool True on success, false if the contents were not deleted.
1633 - * @since 2.0.0
1634 - *
1635 - */
1636 - public function delete( $key, $group = 'default', $deprecated = false ) {
1637 - if ( ! $this->is_valid_key( $key ) ) {
1638 - return false;
1639 - }
1977 + return $group . '|' . $key;
1978 + }
1640 1979
1641 - if ( empty( $group ) ) {
1642 - $group = 'default';
1643 - }
1980 + /**
1981 + * Retrieves the cache contents, if it exists.
1982 + *
1983 + * The contents will be first attempted to be retrieved by searching by the
1984 + * key in the cache group. If the cache is hit (success) then the contents
1985 + * are returned.
1986 + *
1987 + * On failure, the number of cache misses will be incremented.
1988 + *
1989 + * @param int|string $key The key under which the cache contents are stored.
1990 + * @param string $group Optional. Where the cache contents are grouped. Default 'default'.
1991 + * @param bool $force Optional. Whether to force an update of the local cache
1992 + * from the persistent cache. Default false.
1993 + * @param bool $found Optional. Whether the key was found in the cache (passed by reference).
1994 + * Disambiguates a return of false, a storable value. Default null.
1995 + *
1996 + * @return mixed|false The cache contents on success, false on failure to retrieve contents.
1997 + * @since 2.0.0
1998 + */
1999 + public function get( $key, $group = 'default', $force = false, &$found = null ) {
2000 + if ( -- $this->get_depth <= 0 ) {
2001 + return false;
2002 + }
1644 2003
1645 - if ( $this->multisite && ! isset( $this->global_groups[ $group ] ) ) {
1646 - $key = $this->blog_prefix . $key;
1647 - }
2004 + if ( ! $this->is_valid_key( $key ) ) {
2005 + ++ $this->get_depth;
1648 2006
1649 - try {
2007 + return false;
2008 + }
1650 2009
1651 - if ( ! $this->cache_item_exists( $key, $group ) ) {
1652 - return false;
1653 - }
1654 - } catch ( Exception $ex ) {
1655 - $this->delete_offending_files();
2010 + $start = hrtime( true );
2011 + $name = $this->normalize_name( $key, $group );
1656 2012
1657 - return true;
1658 - }
2013 + if ( $force ) {
2014 + unset( $this->cache[ $name ] );
2015 + unset ( $this->not_in_persistent_cache[ $name ] );
2016 + }
1659 2017
1660 - unset( $this->cache[ $group ][ $key ] );
1661 - $this->handle_delete( $key, $group );
2018 + try {
2019 + if ( array_key_exists( $name, $this->cache ) ) {
2020 + $found = true;
2021 + ++ $this->cache_hits;
2022 + ++ $this->get_depth;
1662 2023
1663 - return true;
1664 - }
2024 + return is_object( $this->cache[ $name ] ) ? clone( $this->cache[ $name ] ) : $this->cache[ $name ];
2025 + }
2026 + if ( $this->cache_item_exists( $name ) ) {
2027 + $found = true;
2028 + ++ $this->cache_hits;
2029 + ++ $this->get_depth;
1665 2030
1666 - /**
1667 - * Delete from the persistent cache.
1668 - *
1669 - * @param int|string $key What to call the contents in the cache.
1670 - * @param string $group Optional. Where to group the cache contents. Default 'default'.
1671 - *
1672 - * @return void
1673 - */
1674 - private function handle_delete( $key, $group ) {
1675 - $name = $this->name_from_key_group( $key, $group );
1676 - $start = $this->time_usec();
1677 - $stmt = $this->deleteone;
1678 - try {
1679 - if ( ! $this->sqlite ) {
1680 - $this->open_connection();
1681 - }
1682 - $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
1683 - $result = $stmt->execute();
1684 - $result->finalize();
1685 - } catch ( Exception $ex ) {
1686 - $this->delete_offending_files();
1687 - }
1688 - unset( $this->in_persistent_cache[ $name ] );
1689 - $this->not_in_persistent_cache[ $name ] = true;
1690 - /* track how long it took. */
1691 - $this->delete_times[] = $this->time_usec() - $start;
1692 - }
2031 + $this->get_times[] = hrtime( true ) - $start;
1693 2032
1694 - /**
1695 - * Increments numeric cache item's value.
1696 - *
1697 - * @param int|string $key The cache key to increment.
1698 - * @param int $offset Optional. The amount by which to increment the item's value.
1699 - * Default 1.
1700 - * @param string $group Optional. The group the key is in. Default 'default'.
1701 - *
1702 - * @return int|false The item's new value on success, false on failure.
1703 - * @since 3.3.0
1704 - */
1705 - public function incr( $key, $offset = 1, $group = 'default' ) {
1706 - if ( ! $this->is_valid_key( $key ) ) {
1707 - return false;
1708 - }
2033 + return is_object( $this->cache[ $name ] ) ? clone( $this->cache[ $name ] ) : $this->cache[ $name ];
2034 + }
2035 + } catch ( Exception $ex ) {
2036 + $this->delete_offending_files();
1709 2037
1710 - if ( empty( $group ) ) {
1711 - $group = 'default';
1712 - }
2038 + ++ $this->get_depth;
1713 2039
1714 - if ( $this->multisite && ! isset( $this->global_groups[ $group ] ) ) {
1715 - $key = $this->blog_prefix . $key;
1716 - }
2040 + return false;
2041 + }
1717 2042
1718 - if ( ! $this->cache_item_exists( $key, $group ) ) {
1719 - return false;
1720 - }
2043 + $found = false;
2044 + $this->cache_misses ++;
1721 2045
1722 - if ( ! is_numeric( $this->cache[ $group ][ $key ] ) ) {
1723 - $this->cache[ $group ][ $key ] = 0;
1724 - }
2046 + ++ $this->get_depth;
1725 2047
1726 - $offset = (int) $offset;
2048 + return false;
2049 + }
1727 2050
1728 - $this->cache[ $group ][ $key ] += $offset;
2051 + /**
2052 + * Retrieves the cache contents, if it exists.
2053 + *
2054 + * The contents will be first attempted to be retrieved by searching by the
2055 + * key in the cache group. If the cache is hit (success) then the contents
2056 + * are returned.
2057 + *
2058 + * On failure, the number of cache misses will be incremented.
2059 + *
2060 + * @param string $name Normalized name.
2061 + * @param bool $found Whether the key was found in the cache (passed by reference). Disambiguates a return of false, a storable value.
2062 + *
2063 + * @return mixed|false The cache contents -- clones of objects -- on success, false on failure to retrieve contents.
2064 + * @since 2.0.0
2065 + */
2066 + private function get_by_normalized_name( $name, &$found ) {
2067 + if ( -- $this->get_depth <= 0 ) {
2068 + $found = false;
2069 + return false;
2070 + }
2071 + try {
2072 + if ( array_key_exists( $name, $this->cache ) || $this->cache_item_exists( $name ) ) {
2073 + ++ $this->cache_hits;
2074 + ++ $this->get_depth;
2075 + $found = true;
1729 2076
1730 - if ( $this->cache[ $group ][ $key ] < 0 ) {
1731 - $this->cache[ $group ][ $key ] = 0;
1732 - }
1733 - $this->handle_put( $key, $group, $this->cache[ $group ][ $key ], 0 );
2077 + return is_object( $this->cache[ $name ] ) ? clone( $this->cache[ $name ] ) : $this->cache[ $name ];
2078 + }
2079 + } catch ( Exception $ex ) {
2080 + $this->delete_offending_files();
1734 2081
1735 - return $this->cache[ $group ][ $key ];
1736 - }
2082 + ++ $this->get_depth;
1737 2083
1738 - /**
1739 - * Decrements numeric cache item's value.
1740 - *
1741 - * @param int|string $key The cache key to decrement.
1742 - * @param int $offset Optional. The amount by which to decrement the item's value.
1743 - * Default 1.
1744 - * @param string $group Optional. The group the key is in. Default 'default'.
1745 - *
1746 - * @return int|false The item's new value on success, false on failure.
1747 - * @since 3.3.0
1748 - *
1749 - */
1750 - public function decr( $key, $offset = 1, $group = 'default' ) {
1751 - if ( ! $this->is_valid_key( $key ) ) {
1752 - return false;
1753 - }
2084 + $found = false;
2085 + return false;
2086 + }
2087 + $this->cache_misses ++;
2088 + ++ $this->get_depth;
1754 2089
1755 - if ( empty( $group ) ) {
1756 - $group = 'default';
1757 - }
2090 + $found = false;
2091 + return false;
2092 + }
1758 2093
1759 - if ( $this->multisite && ! isset( $this->global_groups[ $group ] ) ) {
1760 - $key = $this->blog_prefix . $key;
1761 - }
2094 + /**
2095 + * Deletes multiple values from the cache in one call.
2096 + *
2097 + * @param array $keys Array of keys to be deleted.
2098 + * @param string $group Optional. Where the cache contents are grouped. Default empty.
2099 + *
2100 + * @return bool[] Array of return values, grouped by key. Each value is either
2101 + * true on success, or false if the contents were not deleted.
2102 + * @since 6.0.0
2103 + */
2104 + public function delete_multiple( array $keys, $group = '' ) {
2105 + if ( 0 === count( $keys ) ) {
2106 + return array();
2107 + }
2108 + $values = array();
1762 2109
1763 - if ( ! $this->cache_item_exists( $key, $group ) ) {
1764 - return false;
1765 - }
2110 + /* use a transaction to accelerate delete_multiple */
2111 + $transaction_size = self::TRANSACTION_SIZE_LIMIT;
2112 + $this->transaction_active = true;
2113 + $this->sqlite->exec( 'BEGIN' );
1766 2114
1767 - if ( ! is_numeric( $this->cache[ $group ][ $key ] ) ) {
1768 - $this->cache[ $group ][ $key ] = 0;
1769 - }
2115 + foreach ( $keys as $key ) {
2116 + $values[ $key ] = $this->delete( $key, $group );
2117 + /* limit the size of the transaction, hopefully preventing timeouts in other clients */
2118 + if ( -- $transaction_size <= 0 ) {
2119 + $this->sqlite->exec( 'COMMIT' );
2120 + $this->sqlite->exec( 'BEGIN' );
2121 + $transaction_size = self::TRANSACTION_SIZE_LIMIT;
2122 + }
2123 + }
2124 + $this->sqlite->exec( 'COMMIT' );
2125 + $this->transaction_active = false;
1770 2126
1771 - $offset = (int) $offset;
2127 + return $values;
2128 + }
1772 2129
1773 - $this->cache[ $group ][ $key ] -= $offset;
2130 + /**
2131 + * Removes the contents of the cache key in the group.
2132 + *
2133 + * If the cache key does not exist in the group, then nothing will happen.
2134 + *
2135 + * @param int|string $key What the contents in the cache are called.
2136 + * @param string $group Optional. Where the cache contents are grouped. Default 'default'.
2137 + * @param bool $deprecated Optional. Unused. Default false.
2138 + *
2139 + * @return bool True on success, false if the contents were not deleted.
2140 + * @since 2.0.0
2141 + *
2142 + */
2143 + public function delete( $key, $group = 'default', $deprecated = false ) {
2144 + if ( ! $this->is_valid_key( $key ) ) {
2145 + return false;
2146 + }
1774 2147
1775 - if ( $this->cache[ $group ][ $key ] < 0 ) {
1776 - $this->cache[ $group ][ $key ] = 0;
1777 - }
2148 + $name = $this->normalize_name( $key, $group );
2149 + unset ( $this->cache[ $name ] );
2150 + $this->delete_by_name( $name );
1778 2151
1779 - $this->handle_put( $key, $group, $this->cache[ $group ][ $key ], 0 );
2152 + return true;
2153 + }
1780 2154
1781 - return $this->cache[ $group ][ $key ];
1782 - }
2155 + /**
2156 + * Clear the APCu cache.
2157 + * @return void
2158 + */
2159 + public function apcu_clear_cache() {
2160 + /* Immediate cache clear. */
2161 + if ( $this->apcu_active ) {
2162 + foreach ( new APCUIterator( '/^' . $this->apcusalt . '/', APC_ITER_KEY ) as $item ) {
2163 + apcu_delete( $item['key'] );
2164 + }
2165 + }
2166 + /* Deferred cache clear if we're in CLI context. */
2167 + if ( $this->apcu_supported ) {
2168 + $this->set_flag();
2169 + }
2170 + }
1783 2171
1784 - /**
1785 - * Clears the object cache of all data.
1786 - *
1787 - * @param bool $vacuum True to do a VACUUM operation.
1788 - *
1789 - * @return bool Always returns true.
1790 - * @since 2.0.0
1791 - */
1792 - public function flush( $vacuum = false ) {
1793 - /* NOTE WELL: SQL in this file is not for use with $wpdb, but for SQLite3 */
1794 - try {
1795 - if ( ! $this->sqlite ) {
1796 - $this->open_connection();
1797 - }
2172 + /**
2173 + * Delete the oldest elements until the size falls below the target size.
2174 + *
2175 + * This uses a least-recently-UPDATED approach to aging the elements. A least-recently-USED
2176 + * approach requires writing the time of use to the cache with every access, and that
2177 + * is too expensive.
2178 + *
2179 + * @param int $target_size Desired size in bytes.
2180 + * @param int $current_size Current size in bytes.
2181 + *
2182 + * @return void
2183 + */
2184 + public function sqlite_delete_old( $target_size, $current_size ) {
2185 + $horizon = null;
2186 + if ( ! $this->sqlite ) {
2187 + return;
2188 + }
2189 + try {
2190 + if ( $target_size < $current_size ) {
2191 + $resultset = $this->sqlite_load_sizes();
2192 + if ( ! $resultset ) {
2193 + return;
2194 + }
2195 + while ( true ) {
2196 + $row = $resultset->fetchArray( SQLITE3_NUM );
2197 + if ( ! $row ) {
2198 + break;
2199 + }
2200 + /* Find the time horizon that will delete enough entries */
2201 + $horizon = $row[1];
2202 + $current_size -= $row[0];
2203 + if ( $current_size <= $target_size ) {
2204 + break;
2205 + }
2206 + }
2207 + $resultset->finalize();
2208 + if ( ! $horizon ) {
2209 + return;
2210 + }
2211 + $object_cache = self::OBJECT_CACHE_TABLE;
2212 + $offset = $this->noexpire_timestamp_offset;
2213 + $limit = self::TRANSACTION_SIZE_LIMIT;
2214 + $hit = $limit;
2215 + $cleared = false;
1798 2216
1799 - $this->cache = [];
1800 - $this->not_in_persistent_cache = [];
2217 + while ( $hit >= $limit ) {
2218 + if ( ! $cleared ) {
2219 + /* Clear the APCu cache when we bulk-delete entries from SQLite. */
2220 + $this->apcu_clear_cache();
2221 + $cleared = true;
2222 + }
2223 + $sql = "DELETE FROM $object_cache WHERE name IN (SELECT name FROM $object_cache WHERE expires >= $offset AND expires <= $horizon LIMIT $limit)";
2224 + $this->sqlite->exec( $sql );
2225 + $hit = $this->sqlite->changes();
2226 + }
2227 + if ( $cleared ) {
2228 + /* Clear the APCu cache again after bulk delete to avoid a race condition. */
2229 + $this->apcu_clear_cache();
2230 + }
2231 + $this->sqlite->exec( 'PRAGMA optimize;' );
2232 + }
2233 + $this->checkpoint();
2234 + } catch ( Exception $ex ) {
2235 + /* Empty, intentionally. */
2236 + }
2237 + }
1801 2238
1802 - $selective =
1803 - defined( 'WP_SQLITE_OBJECT_CACHE_SELECTIVE_FLUSH' ) ? WP_SQLITE_OBJECT_CACHE_SELECTIVE_FLUSH : null;
2239 + /**
2240 + * Delete from the persistent cache.
2241 + *
2242 + * @param string $name What to call the contents in the cache.
2243 + *
2244 + * @return void
2245 + */
2246 + private function delete_by_name( $name ) {
2247 + $exception = null;
2248 + $retries = 3;
2249 + $stmt = $this->deleteone_stmt;
2250 + $this->not_in_persistent_cache[ $name ] = true;
2251 + $start = hrtime( true );
2252 + if ( $this->apcu_active ) {
2253 + apcu_delete( $this->apcusalt . $name );
2254 + }
2255 + while ( $retries -- > 0 ) {
2256 + try {
2257 + $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
2258 + $result = $stmt->execute();
2259 + $result->finalize();
2260 + $this->delete_times[] = hrtime( true ) - $start;
1804 2261
1805 - if ( $selective && is_array( $this->unflushable_groups ) && count( $this->unflushable_groups ) > 0 ) {
1806 - $clauses = [];
1807 - foreach ( $this->unflushable_groups as $unflushable_group ) {
1808 - $unflushable_group = sanitize_key( $unflushable_group );
1809 - $clauses [] = "(name NOT LIKE '$unflushable_group|%')";
1810 - }
1811 - /* @noinspection SqlConstantCondition, SqlConstantExpression */
1812 - $sql =
1813 - 'DELETE FROM ' . $this->cache_table_name . ' WHERE ' . implode( ' AND ', $clauses ) . ';';
1814 - } else {
1815 - /* SQLite's TRUNCATE TABLE equivalent */
1816 - $sql =
1817 - 'DELETE FROM ' . $this->cache_table_name . ';';
1818 - }
1819 - $this->sqlite->exec( $sql );
2262 + return;
2263 + } catch ( Exception $ex ) {
2264 + $exception = $ex;
2265 + if ( ! str_contains( $ex->getMessage(), 'database is locked' ) || 5 !== $this->sqlite->lastErrorCode() ) {
2266 + break;
2267 + }
2268 + }
2269 + sleep( 1 );
2270 + }
2271 + if ( $exception ) {
2272 + $this->error_log( 'delete_by_name', $exception );
2273 + $this->delete_offending_files();
2274 + self::drop_dead();
2275 + }
2276 + }
1820 2277
1821 - if ( $vacuum ) {
1822 - $this->sqlite->exec( 'VACUUM;' );
1823 - }
1824 - } catch ( Exception $ex ) {
1825 - $this->error_log( 'flush', $ex );
1826 - $this->delete_offending_files();
1827 - self::drop_dead();
1828 - }
2278 + /**
2279 + * Increments numeric cache item's value.
2280 + *
2281 + * @param int|string $key The cache key to increment.
2282 + * @param int $offset Optional. The amount by which to increment the item's value.
2283 + * Default 1.
2284 + * @param string $group Optional. The group the key is in. Default 'default'.
2285 + *
2286 + * @return int|false The item's new value on success, false on failure.
2287 + * @since 3.3.0
2288 + */
2289 + public function incr( $key, $offset = 1, $group = 'default' ) {
2290 + if ( ! $this->is_valid_key( $key ) ) {
2291 + return false;
2292 + }
1829 2293
1830 - return true;
1831 - }
2294 + $name = $this->normalize_name( $key, $group );
1832 2295
1833 - /**
1834 - * Clears the in-memory cache of all data leaving the external cache untouched.
1835 - *
1836 - * @return bool Always returns true.
1837 - * @since 2.0.0
1838 - */
1839 - public function flush_runtime() {
1840 - $this->cache = [];
1841 - $this->not_in_persistent_cache = [];
1842 - $this->in_persistent_cache = [];
2296 + if ( $this->cache_item_not_exists( $name ) ) {
2297 + return false;
2298 + }
1843 2299
1844 - return true;
1845 - }
2300 + if ( ! is_numeric( $this->cache[ $name ] ) ) {
2301 + $this->cache[ $name ] = 0;
2302 + }
1846 2303
1847 - /**
1848 - * Removes all cache items in a group.
1849 - *
1850 - * @param string $group Name of group to remove from cache.
1851 - *
1852 - * @return true Always returns true.
1853 - * @since 6.1.0
1854 - */
1855 - public function flush_group( $group ) {
1856 - try {
1857 - if ( ! $this->sqlite ) {
1858 - $this->open_connection();
1859 - }
2304 + $offset = (int) $offset;
1860 2305
1861 - $start = $this->time_usec();
1862 - unset( $this->cache[ $group ] );
1863 - $stmt = $this->deletegroup;
1864 - $stmt->bindValue( ':group', $group, SQLITE3_TEXT );
1865 - $result = $stmt->execute();
1866 - $result->finalize();
1867 - } catch ( Exception $ex ) {
1868 - $this->error_log( 'flush_group', $ex );
1869 - $this->delete_offending_files();
1870 - self::drop_dead();
1871 - }
1872 - /* remove hints about what is in the persistent cache */
1873 - $this->not_in_persistent_cache = [];
1874 - $this->in_persistent_cache = [];
2306 + $this->cache[ $name ] += $offset;
1875 2307
1876 - return true;
1877 - }
2308 + if ( $this->cache[ $name ] < 0 ) {
2309 + $this->cache[ $name ] = 0;
2310 + }
2311 + $this->put_by_name( $name, $this->cache[ $name ], 0 );
1878 2312
1879 - /**
1880 - * Sets the list of groups not to flushed cached.
1881 - *
1882 - * @param array $groups List of groups that are unflushable.
1883 - */
1884 - public function add_unflushable_groups( $groups ) {
1885 - $groups = (array) $groups;
2313 + return $this->cache[ $name ];
2314 + }
1886 2315
1887 - $this->unflushable_groups = array_unique( array_merge( $this->unflushable_groups, $groups ) );
1888 - $this->cache_group_types();
1889 - }
2316 + /**
2317 + * Decrements numeric cache item's value.
2318 + *
2319 + * @param int|string $key The cache key to decrement.
2320 + * @param int $offset Optional. The amount by which to decrement the item's value.
2321 + * Default 1.
2322 + * @param string $group Optional. The group the key is in. Default 'default'.
2323 + *
2324 + * @return int|false The item's new value on success, false on failure.
2325 + * @since 3.3.0
2326 + *
2327 + */
2328 + public function decr( $key, $offset = 1, $group = 'default' ) {
2329 + return $this->incr( $key, - $offset, $group );
2330 + }
1890 2331
1891 - /**
1892 - * Sets the list of global cache groups.
1893 - *
1894 - * @param string|string[] $groups List of groups that are global.
1895 - *
1896 - * @since 3.0.0
1897 - */
1898 - public function add_global_groups( $groups ) {
1899 - $groups = (array) $groups;
2332 + /**
2333 + * Checkpoint and immediately vacuum.
2334 + *
2335 + * Notice that
2336 + * @return void
2337 + */
2338 + public function vacuum() {
2339 + $this->checkpoint();
2340 + $this->sqlite->exec( 'VACUUM;' );
1900 2341
1901 - $groups = array_fill_keys( $groups, true );
1902 - $this->global_groups = array_merge( $this->global_groups, $groups );
2342 + }
1903 2343
1904 - $this->cache_group_types();
1905 - }
2344 + /**
2345 + * Clears the object cache of all data.
2346 + *
2347 + * @param bool $vacuum True to do a VACUUM operation.
2348 + *
2349 + * @return bool Always returns true.
2350 + * @since 2.0.0
2351 + */
2352 + public function flush( $vacuum = false ) {
2353 + try {
2354 + $this->apcu_clear_cache();
2355 + $this->cache = array();
2356 + $this->not_in_persistent_cache = array();
1906 2357
1907 - /**
1908 - * Switches the internal blog ID.
1909 - *
1910 - * This changes the blog ID used to create keys in blog specific groups.
1911 - *
1912 - * @param int $blog_id Blog ID.
1913 - *
1914 - * @since 3.5.0
1915 - *
1916 - */
1917 - public function switch_to_blog( $blog_id ) {
1918 - $blog_id = (int) $blog_id;
1919 - $this->blog_prefix = $this->multisite ? $blog_id . ':' : '';
1920 - }
2358 + $selective =
2359 + defined( 'WP_SQLITE_OBJECT_CACHE_SELECTIVE_FLUSH' ) ? WP_SQLITE_OBJECT_CACHE_SELECTIVE_FLUSH : null;
1921 2360
1922 - /**
1923 - * Resets cache keys.
1924 - *
1925 - * @since 3.0.0
1926 - *
1927 - * @deprecated 3.5.0 Use WP_Object_Cache::switch_to_blog()
1928 - * @see switch_to_blog()
1929 - */
1930 - public function reset() {
1931 - _deprecated_function( __FUNCTION__, '3.5.0', 'WP_Object_Cache::switch_to_blog()' );
2361 + if ( $selective && is_array( $this->unflushable_groups ) && count( $this->unflushable_groups ) > 0 ) {
2362 + $clauses = array();
2363 + foreach ( $this->unflushable_groups as $unflushable_group ) {
2364 + $unflushable_group = sanitize_key( $unflushable_group );
2365 + $clauses [] = "(name NOT LIKE '$unflushable_group|%')";
2366 + }
2367 + /* @noinspection SqlConstantCondition, SqlConstantExpression */
2368 + $limit = self::TRANSACTION_SIZE_LIMIT;
2369 + $hit = $limit;
2370 + $this->checkpoint();
2371 + $sql = 'DELETE FROM ' . $this->cache_table_name . ' WHERE name IN (SELECT name FROM ' . $this->cache_table_name . ' WHERE ' . implode( ' AND ', $clauses ) . ' LIMIT $limit);';
2372 + while ( $hit >= $limit ) {
2373 + $this->sqlite->exec( $sql );
2374 + $hit = $this->sqlite->changes();
2375 + }
2376 + } else {
2377 + /* SQLite's TRUNCATE TABLE equivalent */
2378 + $sql = 'DELETE FROM ' . $this->cache_table_name . ';';
2379 + $this->sqlite->exec( $sql );
2380 + }
1932 2381
1933 - // Clear out non-global caches since the blog ID has changed.
1934 - foreach ( array_keys( $this->cache ) as $group ) {
1935 - if ( ! isset( $this->global_groups[ $group ] ) ) {
1936 - unset( $this->cache[ $group ] );
1937 - }
1938 - }
1939 - }
2382 + if ( $vacuum ) {
2383 + $this->vacuum();
2384 + }
2385 + } catch ( Exception $ex ) {
2386 + $this->error_log( 'flush failure, recreate cache.', $ex );
2387 + $this->delete_offending_files();
2388 + }
1940 2389
1941 - /**
1942 - * Echoes the stats of the caching.
1943 - *
1944 - * Gives the cache hits, and cache misses. Also prints every cached group,
1945 - * key and the data.
1946 - *
1947 - * @since 2.0.0
1948 - */
1949 - public function stats() {
1950 - echo '<p><strong>Cache Hits:</strong> ' . esc_html( $this->cache_hits ) . '<br />';
1951 - echo '<strong>Cache Misses:</strong> ' . esc_html( $this->cache_misses ) . '<br /></p>' . PHP_EOL;
1952 - echo '<ul>';
1953 - foreach ( $this->cache as $group => $cache ) {
1954 - $length = number_format( strlen( $this->maybe_serialize( $cache ) ) / KB_IN_BYTES, 1 );
1955 - $item = $group . ' - ( ' . $length . 'KiB )';
1956 - echo '<li><strong>Group:</strong> ' . esc_html( $item ) . '</li>';
1957 - }
1958 - echo '</ul>';
1959 - }
2390 + return true;
2391 + }
1960 2392
1961 - /**
1962 - * Return the cache type. For use by "wp-cli cache type" and other display code.
1963 - *
1964 - * @return string The type of cache, "SQLite".
1965 - */
1966 - public function get_cache_type() {
1967 - return 'SQLite';
1968 - }
2393 + /**
2394 + * Clears the in-memory cache of all data leaving the external cache untouched.
2395 + *
2396 + * @return bool Always returns true.
2397 + * @since 2.0.0
2398 + */
2399 + public function flush_runtime() {
2400 + $this->cache = array();
2401 + $this->not_in_persistent_cache = array();
1969 2402
1970 - /**
1971 - * Checks if the given group is part the ignored group array
1972 - *
1973 - * @param string $group Name of the group to check, pre-sanitized.
1974 - *
1975 - * @return bool
1976 - */
1977 - protected function is_ignored_group( $group ) {
1978 - return $this->is_group_of_type( $group, 'ignored' );
1979 - }
2403 + return true;
2404 + }
1980 2405
1981 - /**
1982 - * Checks the type of the given group
1983 - *
1984 - * @param string $group Name of the group to check, pre-sanitized.
1985 - * @param string $type Type of the group to check.
1986 - *
1987 - * @return bool
1988 - */
1989 - private function is_group_of_type( $group, $type ) {
1990 - return isset( $this->group_type[ $group ] ) && $this->group_type[ $group ] === $type;
1991 - }
2406 + /**
2407 + * Removes all cache items in a group.
2408 + *
2409 + * @param string $group Name of group to remove from cache.
2410 + *
2411 + * @return true Always returns true.
2412 + * @since 6.1.0
2413 + */
2414 + public function flush_group( $group ) {
2415 + $this->apcu_clear_cache();
1992 2416
1993 - /**
1994 - * Checks if the given group is part the global group array
1995 - *
1996 - * @param string $group Name of the group to check, pre-sanitized.
1997 - *
1998 - * @return bool
1999 - */
2000 - protected function is_global_group( $group ) {
2001 - return $this->is_group_of_type( $group, 'global' );
2002 - }
2417 + try {
2418 + $names_to_flush = array();
2419 + $prefix = $group . '|';
2420 + foreach ( $this->cache as $name => $data ) {
2421 + if ( str_starts_with( $name, $prefix ) ) {
2422 + $names_to_flush [] = $name;
2423 + }
2424 + }
2425 + foreach ( $names_to_flush as $name ) {
2426 + unset ( $this->cache[ $name ] );
2427 + $this->not_in_persistent_cache[ $name ] = true;
2428 + }
2429 + unset ( $names_to_flush );
2003 2430
2004 - /**
2005 - * Get the names of the SQLite files.
2006 - *
2007 - * Notice there are, possibly, multiple files used to hold sqlite data.
2008 - *
2009 - * @return Generator Name of one of the possible SQLite files.
2010 - */
2011 - public function sqlite_files() {
2012 - foreach ( [ '', '-shm', '-wal' ] as $suffix ) {
2013 - yield $this->sqlite_path . $suffix;
2014 - }
2015 - }
2431 + $stmt = $this->deletegroup_stmt;
2432 + $stmt->bindValue( ':group', $prefix, SQLITE3_TEXT );
2433 + $result = $stmt->execute();
2434 + $result->finalize();
2435 + } catch ( Exception $ex ) {
2436 + $this->error_log( 'flush_group', $ex );
2437 + $this->delete_offending_files();
2438 + }
2439 + /* remove hints about what is in the persistent cache */
2440 + $this->not_in_persistent_cache = array();
2016 2441
2017 - /**
2018 - * Delete sqlite files in hopes of recovering from trouble.
2019 - *
2020 - * @param int $retries
2021 - *
2022 - * @return void
2023 - */
2024 - private function delete_offending_files( $retries = 0 ) {
2025 - error_log( "sqlite_object_cache failure, deleting sqlite files to retry. $retries" );
2026 - require_once ABSPATH . 'wp-admin/includes/file.php';
2027 - ob_start();
2028 - $credentials = request_filesystem_credentials( '' );
2029 - WP_Filesystem( $credentials );
2030 - global $wp_filesystem;
2031 - foreach ( $this->sqlite_files() as $file ) {
2032 - $wp_filesystem->delete( $file );
2033 - }
2034 - ob_end_clean();
2035 - }
2036 - }
2442 + return true;
2443 + }
2037 2444
2038 - /**
2039 - * Object Cache API
2040 - *
2041 - * @link https://developer.wordpress.org/reference/classes/wp_object_cache/
2042 - *
2043 - * @package WordPress
2044 - * @subpackage Cache
2045 - */
2445 + /**
2446 + * Sets the list of groups not to flushed cached.
2447 + *
2448 + * @param array $groups List of groups that are unflushable.
2449 + */
2450 + public function add_unflushable_groups( $groups ) {
2451 + $groups = (array) $groups;
2046 2452
2047 - /**
2048 - * Sets up Object Cache Global and assigns it.
2049 - *
2050 - * @throws RuntimeException If we cannot write the db file into the specified directory.
2051 - * @since 2.0.0
2052 - *
2053 - * @global WP_Object_Cache $wp_object_cache
2054 - */
2055 - function wp_cache_init() {
2056 - $message = WP_Object_Cache::has_sqlite();
2057 - if ( true === $message ) {
2058 - // We need to override this WordPress global in order to inject our cache.
2059 - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
2060 - $GLOBALS['wp_object_cache'] = new WP_Object_Cache();
2061 - } else {
2062 - WP_Object_Cache::drop_dead( $message );
2063 - }
2064 - }
2453 + $this->unflushable_groups = array_unique( array_merge( $this->unflushable_groups, $groups ) );
2454 + $this->cache_group_types();
2455 + }
2065 2456
2066 - /**
2067 - * Adds data to the cache, if the cache key doesn't already exist.
2068 - *
2069 - * @param int|string $key The cache key to use for retrieval later.
2070 - * @param mixed $data The data to add to the cache.
2071 - * @param string $group Optional. The group to add the cache to. Enables the same key
2072 - * to be used across groups. Default empty.
2073 - * @param int $expire Optional. When the cache data should expire, in seconds.
2074 - * Default 0 (no expiration).
2075 - *
2076 - * @return bool True on success, false if cache key and group already exist.
2077 - * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2078 - *
2079 - * @since 2.0.0
2080 - *
2081 - * @see WP_Object_Cache::add()
2082 - */
2083 - function wp_cache_add( $key, $data, $group = '', $expire = 0 ) {
2084 - global $wp_object_cache;
2457 + /**
2458 + * Sets the list of global cache groups.
2459 + *
2460 + * @param string|string[] $groups List of groups that are global.
2461 + *
2462 + * @since 3.0.0
2463 + */
2464 + public function add_global_groups( $groups ) {
2465 + $groups = (array) $groups;
2085 2466
2086 - return $wp_object_cache->add( $key, $data, $group, (int) $expire );
2087 - }
2467 + $groups = array_fill_keys( $groups, true );
2468 + $this->global_groups = array_merge( $this->global_groups, $groups );
2088 2469
2089 - /**
2090 - * Adds multiple values to the cache in one call.
2091 - *
2092 - * @param array $data Array of keys and values to be set.
2093 - * @param string $group Optional. Where the cache contents are grouped. Default empty.
2094 - * @param int $expire Optional. When to expire the cache contents, in seconds.
2095 - * Default 0 (no expiration).
2096 - *
2097 - * @return bool[] Array of return values, grouped by key. Each value is either
2098 - * true on success, or false if cache key and group already exist.
2099 - * @see WP_Object_Cache::add_multiple()
2100 - * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2101 - *
2102 - * @since 6.0.0
2103 - */
2104 - function wp_cache_add_multiple( array $data, $group = '', $expire = 0 ) {
2105 - global $wp_object_cache;
2470 + $this->cache_group_types();
2471 + }
2106 2472
2107 - return $wp_object_cache->add_multiple( $data, $group, $expire );
2108 - }
2473 + /**
2474 + * Switches the internal blog ID.
2475 + *
2476 + * This changes the blog ID used to create keys in blog specific groups.
2477 + *
2478 + * @param int $blog_id Blog ID.
2479 + *
2480 + * @since 3.5.0
2481 + *
2482 + */
2483 + public function switch_to_blog( $blog_id ) {
2484 + $blog_id = (int) $blog_id;
2485 + $this->blog_prefix = $this->multisite ? $blog_id . ':' : '';
2486 + }
2109 2487
2110 - /**
2111 - * Replaces the contents of the cache with new data.
2112 - *
2113 - * @param int|string $key The key for the cache data that should be replaced.
2114 - * @param mixed $data The new data to store in the cache.
2115 - * @param string $group Optional. The group for the cache data that should be replaced.
2116 - * Default empty.
2117 - * @param int $expire Optional. When to expire the cache contents, in seconds.
2118 - * Default 0 (no expiration).
2119 - *
2120 - * @return bool True if contents were replaced, false if original value does not exist.
2121 - * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2122 - *
2123 - * @since 2.0.0
2124 - *
2125 - * @see WP_Object_Cache::replace()
2126 - */
2127 - function wp_cache_replace( $key, $data, $group = '', $expire = 0 ) {
2128 - global $wp_object_cache;
2488 + /**
2489 + * Resets cache keys.
2490 + *
2491 + * @since 3.0.0
2492 + *
2493 + * @deprecated 3.5.0 Use WP_Object_Cache::switch_to_blog()
2494 + * @see switch_to_blog()
2495 + */
2496 + public function reset() {
2497 + _deprecated_function( __FUNCTION__, '3.5.0', 'WP_Object_Cache::switch_to_blog()' );
2129 2498
2130 - return $wp_object_cache->replace( $key, $data, $group, (int) $expire );
2131 - }
2499 + // Clear out non-global caches since the blog ID has changed.
2500 + $names_to_flush = array();
2501 + foreach ( $this->cache as $name => $data ) {
2502 + $splits = explode( '|', $name, 2 );
2503 + if ( 2 === count( $splits ) ) {
2504 + $group = $splits[0];
2505 + if ( ! isset( $this->global_groups[ $group ] ) ) {
2506 + $names_to_flush[] = $name;
2507 + }
2508 + }
2509 + }
2510 + foreach ( $names_to_flush as $name ) {
2511 + unset ( $this->cache[ $name ] );
2512 + $this->not_in_persistent_cache[ $name ] = true;
2513 + }
2514 + }
2132 2515
2133 - /**
2134 - * Saves the data to the cache.
2135 - *
2136 - * Differs from wp_cache_add() and wp_cache_replace() in that it will always write data.
2137 - *
2138 - * @param int|string $key The cache key to use for retrieval later.
2139 - * @param mixed $data The contents to store in the cache.
2140 - * @param string $group Optional. Where to group the cache contents. Enables the same key
2141 - * to be used across groups. Default empty.
2142 - * @param int $expire Optional. When to expire the cache contents, in seconds.
2143 - * Default 0 (no expiration).
2144 - *
2145 - * @return bool True on success, false on failure.
2146 - * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2147 - *
2148 - * @since 2.0.0
2149 - *
2150 - * @see WP_Object_Cache::set()
2151 - */
2152 - function wp_cache_set( $key, $data, $group = '', $expire = 0 ) {
2153 - global $wp_object_cache;
2516 + /**
2517 + * Echoes the stats of the caching.
2518 + *
2519 + * Gives the cache hits, and cache misses. Also prints every cached group,
2520 + * key and the data.
2521 + *
2522 + * @since 2.0.0
2523 + */
2524 + public function stats() {
2525 + echo '<p><strong>Cache Hits:</strong> ' . esc_html( $this->cache_hits ) . '<br />';
2526 + echo '<p><strong>Cache Misses:</strong> ' . esc_html( $this->cache_misses ) . '<br />';
2527 + echo '<p><strong>APCu Hits:</strong> ' . esc_html( $this->apcu_hits ) . '<br />';
2528 + echo '<strong>APCu Misses:</strong> ' . esc_html( $this->apcu_misses ) . '<br /></p>' . PHP_EOL;
2529 + }
2154 2530
2155 - return $wp_object_cache->set( $key, $data, $group, (int) $expire );
2156 - }
2531 + /**
2532 + * Return the cache type. For use by "wp-cli cache type" and other display code.
2533 + *
2534 + * @return string The type of cache, "SQLite".
2535 + */
2536 + public function get_cache_type() {
2537 + return $this->apcu_active ? 'APCu|SQLite' : 'SQLite';
2538 + }
2157 2539
2158 - /**
2159 - * Sets multiple values to the cache in one call.
2160 - *
2161 - * @param array $data Array of keys and values to be set.
2162 - * @param string $group Optional. Where the cache contents are grouped. Default empty.
2163 - * @param int $expire Optional. When to expire the cache contents, in seconds.
2164 - * Default 0 (no expiration).
2165 - *
2166 - * @return bool[] Array of return values, grouped by key. Each value is either
2167 - * true on success, or false on failure.
2168 - * @see WP_Object_Cache::set_multiple()
2169 - * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2170 - *
2171 - * @since 6.0.0
2172 - */
2173 - function wp_cache_set_multiple( array $data, $group = '', $expire = 0 ) {
2174 - global $wp_object_cache;
2540 + /**
2541 + * Checks if the given group is part the ignored group array
2542 + *
2543 + * @param string $group Name of the group to check, pre-sanitized.
2544 + *
2545 + * @return bool
2546 + */
2547 + protected function is_ignored_group( $group ) {
2548 + return $this->is_group_of_type( $group, 'ignored' );
2549 + }
2175 2550
2176 - return $wp_object_cache->set_multiple( $data, $group, $expire );
2177 - }
2551 + /**
2552 + * Checks the type of the given group
2553 + *
2554 + * @param string $group Name of the group to check, pre-sanitized.
2555 + * @param string $type Type of the group to check.
2556 + *
2557 + * @return bool
2558 + */
2559 + private function is_group_of_type( $group, $type ) {
2560 + return isset( $this->group_type[ $group ] ) && $this->group_type[ $group ] === $type;
2561 + }
2178 2562
2179 - /**
2180 - * Retrieves the cache contents from the cache by key and group.
2181 - *
2182 - * @param int|string $key The key under which the cache contents are stored.
2183 - * @param string $group Optional. Where the cache contents are grouped. Default empty.
2184 - * @param bool $force Optional. Whether to force an update of the local cache
2185 - * from the persistent cache. Default false.
2186 - * @param bool $found Optional. Whether the key was found in the cache (passed by reference).
2187 - * Disambiguates a return of false, a storable value. Default null.
2188 - *
2189 - * @return mixed|false The cache contents on success, false on failure to retrieve contents.
2190 - * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2191 - *
2192 - * @since 2.0.0
2193 - *
2194 - * @see WP_Object_Cache::get()
2195 - */
2196 - function wp_cache_get( $key, $group = '', $force = false, &$found = null ) {
2197 - global $wp_object_cache;
2563 + /**
2564 + * Checks if the given group is part the global group array
2565 + *
2566 + * @param string $group Name of the group to check, pre-sanitized.
2567 + *
2568 + * @return bool
2569 + */
2570 + protected function is_global_group( $group ) {
2571 + return $this->is_group_of_type( $group, 'global' );
2572 + }
2198 2573
2199 - return $wp_object_cache->get( $key, $group, $force, $found );
2200 - }
2574 + /**
2575 + * Get the names of the SQLite files.
2576 + *
2577 + * Notice there are, possibly, multiple files used to hold sqlite data.
2578 + *
2579 + * @return Generator Name of one of the possible SQLite files.
2580 + */
2581 + public function sqlite_files() {
2582 + foreach ( array( '', '-shm', '-wal', '-wal2' ) as $suffix ) {
2583 + yield $this->sqlite_path . $suffix;
2584 + }
2585 + }
2201 2586
2202 - /**
2203 - * Retrieves multiple values from the cache in one call.
2204 - *
2205 - * @param array $keys Array of keys under which the cache contents are stored.
2206 - * @param string $group Optional. Where the cache contents are grouped. Default empty.
2207 - * @param bool $force Optional. Whether to force an update of the local cache
2208 - * from the persistent cache. Default false.
2209 - *
2210 - * @return array Array of return values, grouped by key. Each value is either
2211 - * the cache contents on success, or false on failure.
2212 - * @see WP_Object_Cache::get_multiple()
2213 - * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2214 - *
2215 - * @since 5.5.0
2216 - */
2217 - function wp_cache_get_multiple( $keys, $group = '', $force = false ) {
2218 - global $wp_object_cache;
2587 + /**
2588 + * Delete sqlite files in hopes of recovering from trouble.
2589 + *
2590 + * @param int $retries
2591 + *
2592 + * @return void
2593 + */
2594 + private function delete_offending_files( $retries = 0 ) {
2595 + $this->apcu_clear_cache();
2596 + try {
2597 + /* It may be too early to use file.php. */
2598 + if ( false && function_exists( '__' ) ) {
2599 + error_log( "sqlite_object_cache failure, \$wp_filesystem->deleting sqlite files to retry. $retries" );
2600 + ob_start();
2601 + require_once ABSPATH . 'wp-admin/includes/file.php';
2219 2602
2220 - return $wp_object_cache->get_multiple( $keys, $group, $force );
2221 - }
2603 + $credentials = request_filesystem_credentials( '' );
2604 + WP_Filesystem( $credentials );
2605 + global $wp_filesystem;
2606 + foreach ( $this->sqlite_files() as $file ) {
2607 + $wp_filesystem->delete( $file );
2608 + }
2609 + ob_end_clean();
2610 + } else {
2611 + error_log( "sqlite_object_cache failure, unlinking sqlite files to retry. $retries" );
2612 + ob_start();
2613 + foreach ( $this->sqlite_files() as $file ) {
2614 + if ( @file_exists(realpath($file))) {
2615 + @unlink( realpath( $file ) );
2616 + }
2617 + }
2618 + ob_end_clean();
2619 + }
2620 + } catch ( Exception $e ) {
2621 + error_log( "sqlite_object_cache cleanup failure: " . $e->getMessage() );
2222 2622
2223 - /**
2224 - * Removes the cache contents matching key and group.
2225 - *
2226 - * @param int|string $key What the contents in the cache are called.
2227 - * @param string $group Optional. Where the cache contents are grouped. Default empty.
2228 - *
2229 - * @return bool True on successful removal, false on failure.
2230 - * @since 2.0.0
2231 - *
2232 - * @see WP_Object_Cache::delete()
2233 - * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2234 - */
2235 - function wp_cache_delete( $key, $group = '' ) {
2236 - global $wp_object_cache;
2623 + }
2624 + }
2237 2625
2238 - return $wp_object_cache->delete( $key, $group );
2239 - }
2626 + /**
2627 + * Checkpoint the WAL log, incorporating it into the database.
2628 + *
2629 + * This should be done infrequently, but frequently enough so that the log doesn't just keep getting
2630 + * bigger in busy sites with lots of concurrency.
2631 + *
2632 + * @return void
2633 + */
2634 + private function checkpoint() {
2635 + $start = hrtime( true );
2636 + $this->sqlite->exec( 'PRAGMA wal_checkpoint(RESTART)' );
2637 + $this->checkpoint_times[] = 0.000001 * ( hrtime( true ) - $start );
2638 + }
2240 2639
2241 - /**
2242 - * Deletes multiple values from the cache in one call.
2243 - *
2244 - * @param array $keys Array of keys for deletion.
2245 - * @param string $group Optional. Where the cache contents are grouped. Default empty.
2246 - *
2247 - * @return bool[] Array of return values, grouped by key. Each value is either
2248 - * true on success, or false if the contents were not deleted.
2249 - * @since 6.0.0
2250 - *
2251 - * @see WP_Object_Cache::delete_multiple()
2252 - * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2253 - */
2254 - function wp_cache_delete_multiple( array $keys, $group = '' ) {
2255 - global $wp_object_cache;
2640 + /**
2641 + * Set a named flag.
2642 + *
2643 + * @param $name string The name of the flag. Default: 'insert'.
2644 + *
2645 + * @return bool true if the flag was already set, false if it wasn't.
2646 + */
2647 + public function set_flag( $name = 'insert' ) {
2648 + if ( ! $this->setflag_stmt ) {
2649 + $tbl = $this->flags_table_name;
2650 + $this->setflag_stmt =
2651 + $this->sqlite->prepare( "INSERT OR IGNORE INTO $tbl (name) VALUES (:name );" );
2652 + }
2653 + $stmt = $this->setflag_stmt;
2654 + $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
2655 + $result = $stmt->execute();
2656 + $already = 0 === $this->sqlite->changes();
2657 + $result->finalize();
2658 + return $already;
2659 + }
2256 2660
2257 - return $wp_object_cache->delete_multiple( $keys, $group );
2258 - }
2661 + /**
2662 + * Clear a named flag.
2663 + *
2664 + * @param $name string The name of the flag. Default: 'insert'.
2665 + *
2666 + * @return bool true if the flag was set, false if it wasn't.
2667 + */
2668 + public function clear_flag( $name = 'insert' ) {
2669 + if ( ! $this->clearflag_stmt ) {
2670 + $tbl = $this->flags_table_name;
2671 + $this->clearflag_stmt =
2672 + $this->sqlite->prepare( "DELETE FROM $tbl WHERE name = :name;" );
2673 + }
2674 + $stmt = $this->clearflag_stmt;
2675 + $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
2676 + $result = $stmt->execute();
2677 + $already = 1 === $this->sqlite->changes();
2678 + $result->finalize();
2679 + return $already;
2680 + }
2681 + }
2259 2682
2260 - /**
2261 - * Increments numeric cache item's value.
2262 - *
2263 - * @param int|string $key The key for the cache contents that should be incremented.
2264 - * @param int $offset Optional. The amount by which to increment the item's value.
2265 - * Default 1.
2266 - * @param string $group Optional. The group the key is in. Default empty.
2267 - *
2268 - * @return int|false The item's new value on success, false on failure.
2269 - * @see WP_Object_Cache::incr()
2270 - * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2271 - *
2272 - * @since 3.3.0
2273 - */
2274 - function wp_cache_incr( $key, $offset = 1, $group = '' ) {
2275 - global $wp_object_cache;
2683 + /**
2684 + * Object Cache API
2685 + *
2686 + * @link https://developer.wordpress.org/reference/classes/wp_object_cache/
2687 + *
2688 + * @package WordPress
2689 + * @subpackage Cache
2690 + */
2276 2691
2277 - return $wp_object_cache->incr( $key, $offset, $group );
2278 - }
2692 + /**
2693 + * Sets up Object Cache Global and assigns it.
2694 + *
2695 + * @throws RuntimeException If we cannot write the db file into the specified directory.
2696 + * @since 2.0.0
2697 + *
2698 + * @global WP_Object_Cache $wp_object_cache
2699 + */
2700 + function wp_cache_init() {
2701 + $message = WP_Object_Cache::has_sqlite();
2702 + if ( true === $message ) {
2703 + // We need to override this WordPress global in order to inject our cache.
2704 + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
2705 + $GLOBALS['wp_object_cache'] = new WP_Object_Cache();
2706 + } else {
2707 + WP_Object_Cache::drop_dead( $message );
2708 + }
2709 + }
2279 2710
2280 - /**
2281 - * Decrements numeric cache item's value.
2282 - *
2283 - * @param int|string $key The cache key to decrement.
2284 - * @param int $offset Optional. The amount by which to decrement the item's value.
2285 - * Default 1.
2286 - * @param string $group Optional. The group the key is in. Default empty.
2287 - *
2288 - * @return int|false The item's new value on success, false on failure.
2289 - * @see WP_Object_Cache::decr()
2290 - * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2291 - *
2292 - * @since 3.3.0
2293 - */
2294 - function wp_cache_decr( $key, $offset = 1, $group = '' ) {
2295 - global $wp_object_cache;
2711 + /**
2712 + * Adds data to the cache, if the cache key doesn't already exist.
2713 + *
2714 + * @param int|string $key The cache key to use for retrieval later.
2715 + * @param mixed $data The data to add to the cache.
2716 + * @param string $group Optional. The group to add the cache to. Enables the same key
2717 + * to be used across groups. Default empty.
2718 + * @param int $expire Optional. When the cache data should expire, in seconds.
2719 + * Default 0 (no expiration).
2720 + *
2721 + * @return bool True on success, false if cache key and group already exist.
2722 + * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2723 + *
2724 + * @since 2.0.0
2725 + *
2726 + * @see WP_Object_Cache::add()
2727 + */
2728 + function wp_cache_add( $key, $data, $group = '', $expire = 0 ) {
2729 + global $wp_object_cache;
2296 2730
2297 - return $wp_object_cache->decr( $key, $offset, $group );
2298 - }
2731 + return $wp_object_cache->add( $key, $data, $group, (int) $expire );
2732 + }
2299 2733
2300 - /**
2301 - * Removes all cache items.
2302 - *
2303 - * @return bool True on success, false on failure.
2304 - * @see WP_Object_Cache::flush()
2305 - * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2306 - *
2307 - * @since 2.0.0
2308 - *
2309 - */
2310 - function wp_cache_flush() {
2311 - global $wp_object_cache;
2734 + /**
2735 + * Adds multiple values to the cache in one call.
2736 + *
2737 + * @param array $data Array of keys and values to be set.
2738 + * @param string $group Optional. Where the cache contents are grouped. Default empty.
2739 + * @param int $expire Optional. When to expire the cache contents, in seconds.
2740 + * Default 0 (no expiration).
2741 + *
2742 + * @return bool[] Array of return values, grouped by key. Each value is either
2743 + * true on success, or false if cache key and group already exist.
2744 + * @see WP_Object_Cache::add_multiple()
2745 + * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2746 + *
2747 + * @since 6.0.0
2748 + */
2749 + function wp_cache_add_multiple( array $data, $group = '', $expire = 0 ) {
2750 + global $wp_object_cache;
2312 2751
2313 - return $wp_object_cache->flush();
2314 - }
2752 + return $wp_object_cache->add_multiple( $data, $group, $expire );
2753 + }
2315 2754
2316 - /**
2317 - * Removes all cache items from the in-memory runtime cache.
2318 - *
2319 - * @return bool True on success, false on failure.
2320 - * @see WP_Object_Cache::flush()
2321 - *
2322 - * @since 6.0.0
2323 - *
2324 - */
2325 - function wp_cache_flush_runtime() {
2326 - global $wp_object_cache;
2755 + /**
2756 + * Replaces the contents of the cache with new data.
2757 + *
2758 + * @param int|string $key The key for the cache data that should be replaced.
2759 + * @param mixed $data The new data to store in the cache.
2760 + * @param string $group Optional. The group for the cache data that should be replaced.
2761 + * Default empty.
2762 + * @param int $expire Optional. When to expire the cache contents, in seconds.
2763 + * Default 0 (no expiration).
2764 + *
2765 + * @return bool True if contents were replaced, false if original value does not exist.
2766 + * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2767 + *
2768 + * @since 2.0.0
2769 + *
2770 + * @see WP_Object_Cache::replace()
2771 + */
2772 + function wp_cache_replace( $key, $data, $group = '', $expire = 0 ) {
2773 + global $wp_object_cache;
2327 2774
2328 - return $wp_object_cache->flush_runtime();
2329 - }
2775 + return $wp_object_cache->replace( $key, $data, $group, (int) $expire );
2776 + }
2330 2777
2331 - /**
2332 - * Removes all cache items in a group, if the object cache implementation supports it.
2333 - *
2334 - * Before calling this function, always check for group flushing support using the
2335 - * `wp_cache_supports( 'flush_group' )` function.
2336 - *
2337 - * @param string $group Name of group to remove from cache.
2338 - *
2339 - * @return bool True if group was flushed, false otherwise.
2340 - * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2341 - *
2342 - * @since 6.1.0
2343 - *
2344 - * @see WP_Object_Cache::flush_group()
2345 - */
2346 - function wp_cache_flush_group( $group ) {
2347 - global $wp_object_cache;
2778 + /**
2779 + * Saves the data to the cache.
2780 + *
2781 + * Differs from wp_cache_add() and wp_cache_replace() in that it will always write data.
2782 + *
2783 + * @param int|string $key The cache key to use for retrieval later.
2784 + * @param mixed $data The contents to store in the cache.
2785 + * @param string $group Optional. Where to group the cache contents. Enables the same key
2786 + * to be used across groups. Default empty.
2787 + * @param int $expire Optional. When to expire the cache contents, in seconds.
2788 + * Default 0 (no expiration).
2789 + *
2790 + * @return bool True on success, false on failure.
2791 + * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2792 + *
2793 + * @since 2.0.0
2794 + *
2795 + * @see WP_Object_Cache::set()
2796 + */
2797 + function wp_cache_set( $key, $data, $group = '', $expire = 0 ) {
2798 + global $wp_object_cache;
2348 2799
2349 - return $wp_object_cache->flush_group( $group );
2350 - }
2800 + return $wp_object_cache->set( $key, $data, $group, (int) $expire );
2801 + }
2351 2802
2352 - /**
2353 - * Determines whether the object cache implementation supports a particular feature.
2354 - *
2355 - * @param string $feature Name of the feature to check for. Possible values include:
2356 - * 'add_multiple', 'set_multiple', 'get_multiple', 'delete_multiple',
2357 - * 'flush_runtime', 'flush_group'.
2358 - *
2359 - * @return bool True if the feature is supported, false otherwise.
2360 - * @since 6.1.0
2361 - */
2362 - function wp_cache_supports( $feature ) {
2363 - switch ( $feature ) {
2364 - case 'add_multiple':
2365 - case 'set_multiple':
2366 - case 'get_multiple':
2367 - case 'delete_multiple':
2368 - case 'flush_runtime':
2369 - case 'flush_group':
2370 - return true;
2803 + /**
2804 + * Sets multiple values to the cache in one call.
2805 + *
2806 + * @param array $data Array of keys and values to be set.
2807 + * @param string $group Optional. Where the cache contents are grouped. Default empty.
2808 + * @param int $expire Optional. When to expire the cache contents, in seconds.
2809 + * Default 0 (no expiration).
2810 + *
2811 + * @return bool[] Array of return values, grouped by key. Each value is either
2812 + * true on success, or false on failure.
2813 + * @see WP_Object_Cache::set_multiple()
2814 + * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2815 + *
2816 + * @since 6.0.0
2817 + */
2818 + function wp_cache_set_multiple( array $data, $group = '', $expire = 0 ) {
2819 + global $wp_object_cache;
2371 2820
2372 - default:
2373 - return false;
2374 - }
2375 - }
2821 + return $wp_object_cache->set_multiple( $data, $group, $expire );
2822 + }
2376 2823
2377 - /**
2378 - * Closes the cache.
2379 - *
2380 - * This function has ceased to do anything since WordPress 2.5. The
2381 - * functionality was removed along with the rest of the persistent cache.
2382 - *
2383 - * This does not mean that plugins can't implement this function when they need
2384 - * to make sure that the cache is cleaned up after WordPress no longer needs it.
2385 - *
2386 - * @return true Always returns true.
2387 - * @since 2.0.0
2388 - */
2389 - function wp_cache_close() {
2390 - global $wp_object_cache;
2824 + /**
2825 + * Retrieves the cache contents from the cache by key and group.
2826 + *
2827 + * @param int|string $key The key under which the cache contents are stored.
2828 + * @param string $group Optional. Where the cache contents are grouped. Default empty.
2829 + * @param bool $force Optional. Whether to force an update of the local cache
2830 + * from the persistent cache. Default false.
2831 + * @param bool $found Optional. Whether the key was found in the cache (passed by reference).
2832 + * Disambiguates a return of false, a storable value. Default null.
2833 + *
2834 + * @return mixed|false The cache contents on success, false on failure to retrieve contents.
2835 + * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2836 + *
2837 + * @since 2.0.0
2838 + *
2839 + * @see WP_Object_Cache::get()
2840 + */
2841 + function wp_cache_get( $key, $group = '', $force = false, &$found = null ) {
2842 + global $wp_object_cache;
2391 2843
2392 - return $wp_object_cache->close();
2393 - }
2844 + return $wp_object_cache->get( $key, $group, $force, $found );
2845 + }
2394 2846
2395 - /**
2396 - * Adds a group or set of groups to the list of global groups.
2397 - *
2398 - * @param string|string[] $groups A group or an array of groups to add.
2399 - *
2400 - * @see WP_Object_Cache::add_global_groups()
2401 - * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2402 - *
2403 - * @since 2.6.0
2404 - */
2405 - function wp_cache_add_global_groups( $groups ) {
2406 - global $wp_object_cache;
2847 + /**
2848 + * Retrieves multiple values from the cache in one call.
2849 + *
2850 + * @param array $keys Array of keys under which the cache contents are stored.
2851 + * @param string $group Optional. Where the cache contents are grouped. Default empty.
2852 + * @param bool $force Optional. Whether to force an update of the local cache
2853 + * from the persistent cache. Default false.
2854 + *
2855 + * @return array Array of return values, grouped by key. Each value is either
2856 + * the cache contents on success, or false on failure.
2857 + * @see WP_Object_Cache::get_multiple()
2858 + * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2859 + *
2860 + * @since 5.5.0
2861 + */
2862 + function wp_cache_get_multiple( $keys, $group = '', $force = false ) {
2863 + if ( 0 === count( $keys ) ) {
2864 + return array();
2865 + }
2866 + global $wp_object_cache;
2407 2867
2408 - $wp_object_cache->add_global_groups( $groups );
2409 - }
2868 + return $wp_object_cache->get_multiple( $keys, $group, $force );
2869 + }
2410 2870
2411 - /**
2412 - * Adds a group or set of groups to the list of non-persistent groups.
2413 - *
2414 - * @param string|string[] $groups A group or an array of groups to add.
2415 - *
2416 - * @since 2.6.0
2417 - */
2418 - function wp_cache_add_non_persistent_groups( $groups ) {
2871 + /**
2872 + * Removes the cache contents matching key and group.
2873 + *
2874 + * @param int|string $key What the contents in the cache are called.
2875 + * @param string $group Optional. Where the cache contents are grouped. Default empty.
2876 + *
2877 + * @return bool True on successful removal, false on failure.
2878 + * @since 2.0.0
2879 + *
2880 + * @see WP_Object_Cache::delete()
2881 + * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2882 + */
2883 + function wp_cache_delete( $key, $group = '' ) {
2884 + global $wp_object_cache;
2419 2885
2420 - global $wp_object_cache;
2886 + return $wp_object_cache->delete( $key, $group );
2887 + }
2421 2888
2422 - $wp_object_cache->add_non_persistent_groups( $groups );
2423 - }
2889 + /**
2890 + * Deletes multiple values from the cache in one call.
2891 + *
2892 + * @param array $keys Array of keys for deletion.
2893 + * @param string $group Optional. Where the cache contents are grouped. Default empty.
2894 + *
2895 + * @return bool[] Array of return values, grouped by key. Each value is either
2896 + * true on success, or false if the contents were not deleted.
2897 + * @since 6.0.0
2898 + *
2899 + * @see WP_Object_Cache::delete_multiple()
2900 + * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2901 + */
2902 + function wp_cache_delete_multiple( array $keys, $group = '' ) {
2903 + global $wp_object_cache;
2424 2904
2425 - /**
2426 - * Switches the internal blog ID.
2427 - *
2428 - * This changes the blog id used to create keys in blog specific groups.
2429 - *
2430 - * @param int $blog_id Site ID.
2431 - *
2432 - * @see WP_Object_Cache::switch_to_blog()
2433 - * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2434 - *
2435 - * @since 3.5.0
2436 - */
2437 - function wp_cache_switch_to_blog( $blog_id ) {
2438 - global $wp_object_cache;
2905 + return $wp_object_cache->delete_multiple( $keys, $group );
2906 + }
2439 2907
2440 - $wp_object_cache->switch_to_blog( $blog_id );
2441 - }
2908 + /**
2909 + * Increments numeric cache item's value.
2910 + *
2911 + * @param int|string $key The key for the cache contents that should be incremented.
2912 + * @param int $offset Optional. The amount by which to increment the item's value.
2913 + * Default 1.
2914 + * @param string $group Optional. The group the key is in. Default empty.
2915 + *
2916 + * @return int|false The item's new value on success, false on failure.
2917 + * @see WP_Object_Cache::incr()
2918 + * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2919 + *
2920 + * @since 3.3.0
2921 + */
2922 + function wp_cache_incr( $key, $offset = 1, $group = '' ) {
2923 + global $wp_object_cache;
2442 2924
2443 - /**
2444 - * Resets internal cache keys and structures.
2445 - *
2446 - * If the cache back end uses global blog or site IDs as part of its cache keys,
2447 - * this function instructs the back end to reset those keys and perform any cleanup
2448 - * since blog or site IDs have changed since cache init.
2449 - *
2450 - * This function is deprecated. Use wp_cache_switch_to_blog() instead of this
2451 - * function when preparing the cache for a blog switch. For clearing the cache
2452 - * during unit tests, consider using wp_cache_init(). wp_cache_init() is not
2453 - * recommended outside unit tests as the performance penalty for using it is high.
2454 - *
2455 - * @since 3.0.0
2456 - * @deprecated 3.5.0 Use wp_cache_switch_to_blog()
2457 - * @see WP_Object_Cache::reset()
2458 - *
2459 - * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2460 - */
2461 - function wp_cache_reset() {
2462 - _deprecated_function( __FUNCTION__, '3.5.0', 'wp_cache_switch_to_blog()' );
2925 + return $wp_object_cache->incr( $key, $offset, $group );
2926 + }
2463 2927
2464 - global $wp_object_cache;
2928 + /**
2929 + * Decrements numeric cache item's value.
2930 + *
2931 + * @param int|string $key The cache key to decrement.
2932 + * @param int $offset Optional. The amount by which to decrement the item's value.
2933 + * Default 1.
2934 + * @param string $group Optional. The group the key is in. Default empty.
2935 + *
2936 + * @return int|false The item's new value on success, false on failure.
2937 + * @see WP_Object_Cache::decr()
2938 + * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2939 + *
2940 + * @since 3.3.0
2941 + */
2942 + function wp_cache_decr( $key, $offset = 1, $group = '' ) {
2943 + global $wp_object_cache;
2465 2944
2466 - $wp_object_cache->reset();
2467 - }
2945 + return $wp_object_cache->decr( $key, $offset, $group );
2946 + }
2947 +
2948 + /**
2949 + * Removes all cache items.
2950 + *
2951 + * @return bool True on success, false on failure.
2952 + * @see WP_Object_Cache::flush()
2953 + * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2954 + *
2955 + * @since 2.0.0
2956 + *
2957 + */
2958 + function wp_cache_flush() {
2959 + global $wp_object_cache;
2960 +
2961 + return $wp_object_cache->flush();
2962 + }
2963 +
2964 + /**
2965 + * Removes all cache items from the in-memory runtime cache.
2966 + *
2967 + * @return bool True on success, false on failure.
2968 + * @see WP_Object_Cache::flush()
2969 + *
2970 + * @since 6.0.0
2971 + *
2972 + */
2973 + function wp_cache_flush_runtime() {
2974 + global $wp_object_cache;
2975 +
2976 + return $wp_object_cache->flush_runtime();
2977 + }
2978 +
2979 + /**
2980 + * Removes all cache items in a group, if the object cache implementation supports it.
2981 + *
2982 + * Before calling this function, always check for group flushing support using the
2983 + * `wp_cache_supports( 'flush_group' )` function.
2984 + *
2985 + * @param string $group Name of group to remove from cache.
2986 + *
2987 + * @return bool True if group was flushed, false otherwise.
2988 + * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2989 + *
2990 + * @since 6.1.0
2991 + *
2992 + * @see WP_Object_Cache::flush_group()
2993 + */
2994 + function wp_cache_flush_group( $group ) {
2995 + global $wp_object_cache;
2996 +
2997 + return $wp_object_cache->flush_group( $group );
2998 + }
2999 +
3000 + /**
3001 + * Determines whether the object cache implementation supports a particular feature.
3002 + *
3003 + * @param string $feature Name of the feature to check for. Possible values include:
3004 + * 'add_multiple', 'set_multiple', 'get_multiple', 'delete_multiple',
3005 + * 'flush_runtime', 'flush_group'.
3006 + *
3007 + * @return bool True if the feature is supported, false otherwise.
3008 + * @since 6.1.0
3009 + */
3010 + function wp_cache_supports( $feature ) {
3011 + switch ( $feature ) {
3012 + case 'add_multiple':
3013 + case 'set_multiple':
3014 + case 'get_multiple':
3015 + case 'delete_multiple':
3016 + case 'flush_runtime':
3017 + case 'flush_group':
3018 + return true;
3019 +
3020 + default:
3021 + return false;
3022 + }
3023 + }
3024 +
3025 + /**
3026 + * Closes the cache.
3027 + *
3028 + * This function has ceased to do anything since WordPress 2.5. The
3029 + * functionality was removed along with the rest of the persistent cache.
3030 + *
3031 + * This does not mean that plugins can't implement this function when they need
3032 + * to make sure that the cache is cleaned up after WordPress no longer needs it.
3033 + *
3034 + * @return true Always returns true.
3035 + * @since 2.0.0
3036 + */
3037 + function wp_cache_close() {
3038 + global $wp_object_cache;
3039 +
3040 + return $wp_object_cache->close();
3041 + }
3042 +
3043 + /**
3044 + * Adds a group or set of groups to the list of global groups.
3045 + *
3046 + * @param string|string[] $groups A group or an array of groups to add.
3047 + *
3048 + * @see WP_Object_Cache::add_global_groups()
3049 + * @global WP_Object_Cache $wp_object_cache Object cache global instance.
3050 + *
3051 + * @since 2.6.0
3052 + */
3053 + function wp_cache_add_global_groups( $groups ) {
3054 + global $wp_object_cache;
3055 +
3056 + $wp_object_cache->add_global_groups( $groups );
3057 + }
3058 +
3059 + /**
3060 + * Adds a group or set of groups to the list of non-persistent groups.
3061 + *
3062 + * @param string|string[] $groups A group or an array of groups to add.
3063 + *
3064 + * @since 2.6.0
3065 + */
3066 + function wp_cache_add_non_persistent_groups( $groups ) {
3067 +
3068 + global $wp_object_cache;
3069 +
3070 + $wp_object_cache->add_non_persistent_groups( $groups );
3071 + }
3072 +
3073 + /**
3074 + * Switches the internal blog ID.
3075 + *
3076 + * This changes the blog id used to create keys in blog specific groups.
3077 + *
3078 + * @param int $blog_id Site ID.
3079 + *
3080 + * @see WP_Object_Cache::switch_to_blog()
3081 + * @global WP_Object_Cache $wp_object_cache Object cache global instance.
3082 + *
3083 + * @since 3.5.0
3084 + */
3085 + function wp_cache_switch_to_blog( $blog_id ) {
3086 + global $wp_object_cache;
3087 +
3088 + $wp_object_cache->switch_to_blog( $blog_id );
3089 + }
3090 +
3091 + /**
3092 + * Resets internal cache keys and structures.
3093 + *
3094 + * If the cache back end uses global blog or site IDs as part of its cache keys,
3095 + * this function instructs the back end to reset those keys and perform any cleanup
3096 + * since blog or site IDs have changed since cache init.
3097 + *
3098 + * This function is deprecated. Use wp_cache_switch_to_blog() instead of this
3099 + * function when preparing the cache for a blog switch. For clearing the cache
3100 + * during unit tests, consider using wp_cache_init(). wp_cache_init() is not
3101 + * recommended outside unit tests as the performance penalty for using it is high.
3102 + *
3103 + * @since 3.0.0
3104 + * @deprecated 3.5.0 Use wp_cache_switch_to_blog()
3105 + * @see WP_Object_Cache::reset()
3106 + *
3107 + * @global WP_Object_Cache $wp_object_cache Object cache global instance.
3108 + */
3109 + function wp_cache_reset() {
3110 + _deprecated_function( __FUNCTION__, '3.5.0', 'wp_cache_switch_to_blog()' );
3111 +
3112 + global $wp_object_cache;
3113 +
3114 + $wp_object_cache->reset();
3115 + }
2468 3116 endif;
2469 3117 // phpcs:enable Generic.WhiteSpace.ScopeIndent.IncorrectExact, Generic.WhiteSpace.ScopeIndent.Incorrect