File: //scripts/object-cache.memcached.php
<?php
/**
* Plugin Name: Memcached
* Description: Memcached backend for the WordPress object cache (wp-ok-cache).
* Version: 1.0.0
* Author: Automattic
* License: GPL version 2 or later - http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*/
// Users with setups where multiple installs share a common wp-config.php or $table_prefix
// can use this to guarantee uniqueness for the keys generated by this object cache
if ( ! defined( 'WP_CACHE_KEY_SALT' ) ) {
define( 'WP_CACHE_KEY_SALT', '' );
}
/**
* Helps keep track of cache-related stats.
*
* TODO: Once public access to these methods/properties is deprecated in the main class,
* we can cleanup the data structures internally here.
*/
class WP_Object_Cache_Stats {
/** @var array<string,int> */
public array $stats = [];
/**
* @psalm-var array<string, array<array{0: string, 1: string|string[], 2: int|null, 3: float|null, 4: string, 5: string, 6: string|null }>>
*/
public array $group_ops = [];
public float $time_total = 0;
public int $size_total = 0;
public float $slow_op_microseconds = 0.005; // 5 ms
private string $key_salt;
public function __construct( string $key_salt ) {
$this->key_salt = $key_salt;
$this->stats = [
'get' => 0,
'get_local' => 0,
'get_multi' => 0,
'set' => 0,
'set_local' => 0,
'add' => 0,
'delete' => 0,
'delete_local' => 0,
'slow-ops' => 0,
];
}
/*
|--------------------------------------------------------------------------
| Stat tracking.
|--------------------------------------------------------------------------
*/
/**
* Keep stats for a memcached operation.
*
* @param string $op The operation taking place, such as "set" or "get".
* @param string|string[] $keys The memcached key/keys involved in the operation.
* @param string $group The group the keys are in.
* @param ?int $size The size of the data invovled in the operation.
* @param ?float $time The time the operation took.
* @param string $comment Extra notes about the operation.
*
* @return void
*/
public function group_ops_stats( $op, $keys, $group, $size = null, $time = null, $comment = '' ) {
$this->increment_stat( $op );
// Don't keep further details about the local operations.
if ( false !== strpos( $op, '_local' ) ) {
return;
}
if ( ! is_null( $size ) ) {
$this->size_total += $size;
}
if ( ! is_null( $time ) ) {
$this->time_total += $time;
}
$keys = $this->strip_memcached_keys( $keys );
if ( $time > $this->slow_op_microseconds && 'get_multi' !== $op ) {
$this->increment_stat( 'slow-ops' );
/** @psalm-var string|null $backtrace */
$backtrace = function_exists( 'wp_debug_backtrace_summary' ) ? wp_debug_backtrace_summary() : null; // phpcs:ignore
$this->group_ops['slow-ops'][] = array( $op, $keys, $size, $time, $comment, $group, $backtrace );
}
$this->group_ops[ $group ][] = array( $op, $keys, $size, $time, $comment );
}
/**
* Increment the stat counter for a memcached operation.
*
* @param string $field The stat field/group being incremented.
* @param int $num Amount to increment by.
*
* @return void
*/
public function increment_stat( $field, $num = 1 ) {
if ( ! isset( $this->stats[ $field ] ) ) {
$this->stats[ $field ] = $num;
} else {
$this->stats[ $field ] += $num;
}
}
/*
|--------------------------------------------------------------------------
| Utils.
|--------------------------------------------------------------------------
*/
/**
* Key format: key_salt:flush_number:table_prefix:key_name
*
* We want to strip the `key_salt:flush_number` part to not leak the memcached keys.
* If `key_salt` is set we strip `'key_salt:flush_number`, otherwise just strip the `flush_number` part.
*
* @param string|string[] $keys
* @return string|string[]
*/
public function strip_memcached_keys( $keys ) {
$keys = is_array( $keys ) ? $keys : [ $keys ];
foreach ( $keys as $index => $value ) {
$offset = 0;
// Strip off the key salt piece.
if ( ! empty( $this->key_salt ) ) {
$salt_piece = strpos( $value, ':' );
$offset = false === $salt_piece ? 0 : $salt_piece + 1;
}
// Strip off the flush number.
$flush_piece = strpos( $value, ':', $offset );
$start = false === $flush_piece ? $offset : $flush_piece;
$keys[ $index ] = substr( $value, $start + 1 );
}
if ( 1 === count( $keys ) ) {
return $keys[0];
}
return $keys;
}
/*
|--------------------------------------------------------------------------
| Stats markup output.
|--------------------------------------------------------------------------
*/
/**
* Returns the collected raw stats.
*/
public function get_stats(): array {
$stats = [
'operation_counts' => $this->stats,
'operations' => [],
'groups' => [],
'slow-ops' => [],
'slow-ops-groups' => [],
'totals' => [
'query_time' => $this->time_total,
'size' => $this->size_total,
],
];
foreach ( $this->group_ops as $cache_group => $dataset ) {
$cache_group = empty( $cache_group ) ? 'default' : $cache_group;
foreach ( $dataset as $data ) {
$operation = $data[0];
$op = [
'key' => $data[1],
'size' => $data[2],
'time' => $data[3],
'group' => $cache_group,
'result' => $data[4],
];
if ( 'slow-ops' === $cache_group ) {
$key = 'slow-ops';
$groups_key = 'slow-ops-groups';
$op['group'] = $data[5];
$op['backtrace'] = $data[6];
} else {
$key = 'operations';
$groups_key = 'groups';
}
$stats[ $key ][ $operation ][] = $op;
if ( ! in_array( $op['group'], $stats[ $groups_key ], true ) ) {
$stats[ $groups_key ][] = $op['group'];
}
}
}
return $stats;
}
public function stats(): void {
$this->js_toggle();
$total_ms = $this->time_total * 1000.0;
$total_query_time = number_format_i18n( $total_ms, 1 ) . ' ms';
$total_size = size_format( $this->size_total, 2 );
$total_size = false === $total_size ? '0 B' : $total_size;
echo '<h2><span>Total memcached query time:</span>' . esc_html( $total_query_time ) . '</h2>';
echo "\n";
echo '<h2><span>Total memcached size:</span>' . esc_html( $total_size ) . '</h2>';
echo "\n";
foreach ( $this->stats as $stat => $n ) {
if ( empty( $n ) ) {
continue;
}
echo '<h2>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->colorize_debug_line( "$stat $n" );
echo '</h2>';
}
echo "<ul class='debug-menu-links' style='clear:left;font-size:14px;'>\n";
$groups = array_keys( $this->group_ops );
usort( $groups, 'strnatcasecmp' );
$active_group = $groups[0];
// Always show `slow-ops` first
if ( in_array( 'slow-ops', $groups ) ) {
$slow_ops_key = array_search( 'slow-ops', $groups );
$slow_ops = $groups[ $slow_ops_key ];
unset( $groups[ $slow_ops_key ] );
array_unshift( $groups, $slow_ops );
$active_group = 'slow-ops';
}
$total_ops = 0;
$group_titles = array();
foreach ( $groups as $group ) {
$group_name = empty( $group ) ? 'default' : $group;
$group_size = array_sum( array_map( fn( $op ) => $op[2] ?? 0, $this->group_ops[ $group ] ) );
$group_time_ms = (float) array_sum( array_map( fn( $op ) => $op[3] ?? 0.0, $this->group_ops[ $group ] ) ) * 1000.0;
$group_ops = count( $this->group_ops[ $group ] );
$group_size = size_format( $group_size, 2 );
$group_time = number_format_i18n( $group_time_ms, 1 );
$total_ops += $group_ops;
$group_title = "{$group_name} [$group_ops][$group_size][{$group_time} ms]";
$group_titles[ $group ] = $group_title;
echo "\t<li><a href='#' onclick='memcachedToggleVisibility( \"object-cache-stats-menu-target-" . esc_js( $group_name ) . "\", \"object-cache-stats-menu-target-\" );'>" . esc_html( $group_title ) . "</a></li>\n";
}
echo "</ul>\n";
echo "<div id='object-cache-stats-menu-targets'>\n";
foreach ( $groups as $group ) {
$group_name = empty( $group ) ? 'default' : $group;
$current = $active_group == $group ? 'style="display: block"' : 'style="display: none"';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo "<div id='object-cache-stats-menu-target-" . esc_attr( $group_name ) . "' class='object-cache-stats-menu-target' $current>\n";
echo '<h3>' . esc_html( $group_titles[ $group ] ) . '</h3>' . "\n";
echo "<pre>\n";
foreach ( $this->group_ops[ $group ] as $index => $arr ) {
echo esc_html( sprintf( '%3d ', $index ) );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->get_group_ops_line( $index, $arr );
}
echo "</pre>\n";
echo '</div>';
}
echo '</div>';
}
public function js_toggle(): void {
echo "
<script>
function memcachedToggleVisibility( id, hidePrefix ) {
var element = document.getElementById( id );
if ( ! element ) {
return;
}
// Hide all element with `hidePrefix` if given. Used to display only one element at a time.
if ( hidePrefix ) {
var groupStats = document.querySelectorAll( '[id^=\"' + hidePrefix + '\"]' );
groupStats.forEach(
function ( element ) {
element.style.display = 'none';
}
);
}
// Toggle the one we clicked.
if ( 'none' === element.style.display ) {
element.style.display = 'block';
} else {
element.style.display = 'none';
}
}
</script>
";
}
/**
* @param string $line
* @param string $trailing_html
* @return string
*/
public function colorize_debug_line( $line, $trailing_html = '' ) {
$colors = array(
'get' => 'green',
'get_local' => 'lightgreen',
'get_multi' => 'fuchsia',
'get_multiple' => 'navy',
'set' => 'purple',
'set_local' => 'orchid',
'add' => 'blue',
'delete' => 'red',
'delete_local' => 'tomato',
'slow-ops' => 'crimson',
);
$cmd = substr( $line, 0, (int) strpos( $line, ' ' ) );
// Start off with a neutral default color, and use a more specific one if possible.
$color_for_cmd = isset( $colors[ $cmd ] ) ? $colors[ $cmd ] : 'brown';
$cmd2 = "<span style='color:" . esc_attr( $color_for_cmd ) . "; font-weight: bold;'>" . esc_html( $cmd ) . '</span>';
return $cmd2 . esc_html( substr( $line, strlen( $cmd ) ) ) . "$trailing_html\n";
}
/**
* @param string|int $index
* @param array $arr
* @psalm-param array{0: string, 1: string|string[], 2: int|null, 3: float|null, 4: string, 5: string, 6: string|null } $arr
*
* @return string
*/
public function get_group_ops_line( $index, $arr ) {
// operation
$line = "{$arr[0]} ";
// keys
// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
$json_encoded_key = json_encode( $arr[1] );
if ( false === $json_encoded_key ) {
// Fallback to a readable marker so Psalm knows this is always a string.
$json_encoded_key = '"<unencodable key>"';
}
$line .= $json_encoded_key . ' ';
// comment
if ( ! empty( $arr[4] ) ) {
$line .= "{$arr[4]} ";
}
// size
if ( isset( $arr[2] ) ) {
$size = size_format( $arr[2], 2 );
if ( $size ) {
$line .= '(' . $size . ') ';
}
}
// time
if ( isset( $arr[3] ) ) {
$ms = $arr[3] * 1000.0;
$line .= '(' . number_format_i18n( $ms, 1 ) . ' ms)';
}
// backtrace
$bt_link = '';
if ( isset( $arr[6] ) ) {
$key_hash = md5( (string) $index . $json_encoded_key );
$bt_link = " <small><a href='#' onclick='memcachedToggleVisibility( \"object-cache-stats-debug-$key_hash\" );'>Toggle Backtrace</a></small>";
$bt_link .= "<pre id='object-cache-stats-debug-$key_hash' style='display:none'>" . esc_html( $arr[6] ) . '</pre>';
}
return $this->colorize_debug_line( $line, $bt_link );
}
}
#[\AllowDynamicProperties]
// phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound
class WP_Object_Cache {
public string $flush_group = 'WP_Object_Cache';
public string $global_flush_group = 'WP_Object_Cache_global';
public string $flush_key = 'flush_number_v4';
/**
* Keep track of flush numbers.
* The array key is the blog prefix and value is the number.
* @var array<string,int>
*/
public array $flush_number = [];
public ?int $global_flush_number = null;
public string $global_prefix = '';
public string $blog_prefix = '';
public string $key_salt = '';
/**
* Global cache groups (network-wide rather than site-specific).
* @var string[]
*/
public array $global_groups = [];
/**
* Non-persistent cache groups (will not write to Memcached servers).
* @var string[]
*/
public array $no_mc_groups = [];
public int $default_expiration = 0;
public int $max_expiration = 2592000; // 30 days
private WP_Object_Cache_Stats $stats_helper;
/** @var array<string,string> server_string => server_key */
private array $redundancy_server_keys = [];
/** @psalm-var array<string,\Memcached> */
public array $mc = [];
/** @psalm-var array<int,\Memcached> */
public array $default_mcs = [];
/**
* @psalm-var array<string, array{value: mixed, found: bool}>
*/
public array $cache = [];
// Stats tracking.
public array $stats = [];
public array $group_ops = [];
public int $cache_hits = 0;
public int $cache_misses = 0;
public float $time_start = 0;
public float $time_total = 0;
public int $size_total = 0;
public float $slow_op_microseconds = 0.005; // 5 ms
// TODO: Deprecate. These appear to be unused.
public string $old_flush_key = 'flush_number';
public bool $cache_enabled = true;
public array $stats_callback = [];
/** @psalm-var array<array{host: string, port: string}> */
public array $connection_errors = [];
/**
* @global array<string,array<string>>|array<int,string>|null $memcached_servers
* @global string|null $table_prefix
* @global int|numeric-string $blog_id
*
* @psalm-suppress UnsupportedReferenceUsage
*/
public function __construct() {
global $blog_id, $table_prefix, $memcached_servers;
$this->global_groups = [ $this->global_flush_group ];
$is_ms = function_exists( 'is_multisite' ) && is_multisite();
$this->global_prefix = (string) ( $is_ms || ( defined( 'CUSTOM_USER_TABLE' ) && defined( 'CUSTOM_USER_META_TABLE' ) ) ? '' : $table_prefix );
$this->blog_prefix = (string) ( $is_ms ? $blog_id : $table_prefix );
$servers = is_array( $memcached_servers ) ? $memcached_servers : [ 'default' => [ '127.0.0.1:11211' ] ];
if ( is_int( key( $servers ) ) ) {
// Normalize "list form" into buckets.
$servers = [ 'default' => $servers ];
}
$this->salt_keys( WP_CACHE_KEY_SALT, true );
// Build a Memcached pool per bucket, and individual default server pools for redundancy stats/BC.
$this->mc = [];
$this->default_mcs = [];
$this->connection_errors = []; // kept for BC, still populated by failure_callback()
/** @psalm-var array<string,array<string>> $servers */
foreach ( $servers as $bucket => $addresses ) {
$bucket_servers = [];
foreach ( $addresses as $index => $address ) {
$parsed = $this->parse_address( $address );
$server = [
'host' => $parsed['host'],
'port' => $parsed['port'],
'weight' => 1,
];
$bucket_servers[] = $server;
// Prepare individual connections to servers in the default bucket for flush_number redundancy.
if ( 'default' === $bucket ) {
// Deprecated in this adapter. As long as no requests are made from these pools, the connections should never be established.
$this->default_mcs[] = $this->create_connection_pool( 'redundancy-' . $index, [ $server ] );
}
}
$this->mc[ $bucket ] = $this->create_connection_pool( 'bucket-' . $bucket, $bucket_servers );
}
// Fallback: ensure a default pool exists even if a config misses it.
if ( ! isset( $this->mc['default'] ) && ! empty( $this->mc ) ) {
$first = reset( $this->mc );
$this->mc['default'] = $first;
}
// After default pool exists, precompute server keys for redundancy by consistent hashing.
$this->redundancy_server_keys = $this->get_server_keys_for_redundancy();
$this->stats_helper = new WP_Object_Cache_Stats( $this->key_salt );
// Also for backwards compatibility since these have been public properties.
$this->stats =& $this->stats_helper->stats;
$this->group_ops =& $this->stats_helper->group_ops;
$this->time_total =& $this->stats_helper->time_total;
$this->size_total =& $this->stats_helper->size_total;
$this->slow_op_microseconds =& $this->stats_helper->slow_op_microseconds;
$this->cache_hits =& $this->stats['get'];
$this->cache_misses =& $this->stats['add'];
}
/*
|--------------------------------------------------------------------------
| The main methods used by the cache API.
|--------------------------------------------------------------------------
*/
/**
* Adds data to the cache if it doesn't already exist.
*
* @param int|string $key What to call the contents in the cache.
* @param mixed $data The contents to store in the cache.
* @param string $group Optional. Where to group the cache contents. Default 'default'.
* @param int $expire Optional. When to expire the cache contents, in seconds.
* Default 0 (no expiration).
* @return bool True on success, false on failure or if cache key and group already exist.
*/
public function add( $key, $data, $group = 'default', $expire = 0 ) {
$key = $this->key( $key, $group );
if ( is_object( $data ) ) {
$data = clone $data;
}
if ( $this->is_non_persistent_group( $group ) ) {
if ( isset( $this->cache[ $key ] ) ) {
return false;
}
$this->cache[ $key ] = [
'value' => $data,
'found' => false,
];
return true;
}
if ( isset( $this->cache[ $key ]['value'] ) && false !== $this->cache[ $key ]['value'] ) {
return false;
}
$expire = $this->get_expiration( $expire );
$size = $this->get_data_size( $data );
$this->timer_start();
$mc = $this->get_mc( $group );
$result = $mc->add( $this->normalize_key( $key ), $data, $expire );
$elapsed = $this->timer_stop();
$comment = '';
if ( isset( $this->cache[ $key ] ) ) {
$comment .= ' [lc already]';
}
if ( false === $result ) {
$comment .= ' [mc already]';
}
$this->group_ops_stats( 'add', $key, $group, $size, $elapsed, $comment );
if ( $result ) {
$this->cache[ $key ] = [
'value' => $data,
'found' => true,
];
} elseif ( isset( $this->cache[ $key ]['value'] ) && false === $this->cache[ $key ]['value'] ) {
/*
* Here we unset local cache if remote add failed and local cache value is equal to `false` in order
* to update the local cache anytime we get a new information from remote server. This way, the next
* cache get will go to remote server and will fetch recent data.
*/
unset( $this->cache[ $key ] );
}
return $result;
}
/**
* Adds multiple values to the cache in one call.
*
* @param mixed[] $data Array of keys and values to be added.
* @param string $group Optional. Where the cache contents are grouped. Default empty.
* @param int $expire Optional. When to expire the cache contents, in seconds.
* Default 0 (no expiration).
* @return bool[] Array of return values, grouped by key. Each value is either
* true on success, or false on failure or if cache key and group already exist.
*/
public function add_multiple( $data, $group = '', $expire = 0 ) {
$result = [];
/** @psalm-suppress MixedAssignment - $value is unknown/mixed */
foreach ( $data as $key => $value ) {
$result[ $key ] = $this->add( $key, $value, $group, $expire );
}
return $result;
}
/**
* Replaces the contents in the cache, if contents already exist.
*
* @param int|string $key What to call the contents in the cache.
* @param mixed $data The contents to store in the cache.
* @param string $group Optional. Where to group the cache contents. Default 'default'.
* @param int $expire Optional. When to expire the cache contents, in seconds.
* Default 0 (no expiration).
* @return bool True if contents were replaced, false on failure or if the original value did not exist.
*/
public function replace( $key, $data, $group = 'default', $expire = 0 ) {
$key = $this->key( $key, $group );
if ( is_object( $data ) ) {
$data = clone $data;
}
if ( $this->is_non_persistent_group( $group ) ) {
if ( ! isset( $this->cache[ $key ] ) ) {
return false;
}
$this->cache[ $key ]['value'] = $data;
return true;
}
$expire = $this->get_expiration( $expire );
$size = $this->get_data_size( $data );
$this->timer_start();
$mc = $this->get_mc( $group );
$result = $mc->replace( $this->normalize_key( $key ), $data, $expire );
$elapsed = $this->timer_stop();
$this->group_ops_stats( 'replace', $key, $group, $size, $elapsed );
if ( $result ) {
$this->cache[ $key ] = [
'value' => $data,
'found' => true,
];
} else {
// Remove from local cache if the replace failed, as it may no longer exist.
unset( $this->cache[ $key ] );
}
return $result;
}
/**
* Sets the data contents into the cache.
*
* @param int|string $key What to call the contents in the cache.
* @param mixed $data The contents to store in the cache.
* @param string $group Optional. Where to group the cache contents. Default 'default'.
* @param int $expire Optional. How long until the cahce contents will expire (in seconds).
*
* @return bool True if contents were set, false if failed.
*/
public function set( $key, $data, $group = 'default', $expire = 0 ) {
$key = $this->key( $key, $group );
if ( isset( $this->cache[ $key ] ) && 'checkthedatabaseplease' === $this->cache[ $key ]['value'] ) {
return false;
}
if ( is_object( $data ) ) {
$data = clone $data;
}
if ( $this->is_non_persistent_group( $group ) ) {
$this->group_ops_stats( 'set_local', $key, $group );
$this->cache[ $key ] = [
'value' => $data,
'found' => false,
];
return true;
}
$expire = $this->get_expiration( $expire );
$size = $this->get_data_size( $data );
$this->timer_start();
$mc = $this->get_mc( $group );
$result = $mc->set( $this->normalize_key( $key ), $data, $expire );
$elapsed = $this->timer_stop();
$this->group_ops_stats( 'set', $key, $group, $size, $elapsed );
$this->cache[ $key ] = [
'value' => $data,
'found' => $result,
];
return $result;
}
/**
* Sets multiple values to the cache in one call.
*
* @param mixed[] $data Array of key and value to be set.
* @param string $group Optional. Where the cache contents are grouped. Default empty.
* @param int $expire Optional. When to expire the cache contents, in seconds.
* Default 0 (no expiration).
* @return bool[] Array of return values, grouped by key. Value is true on success, false on failure.
*/
public function set_multiple( $data, $group = '', $expire = 0 ) {
$result = [];
/** @psalm-suppress MixedAssignment - $value is mixed */
foreach ( $data as $key => $value ) {
// TODO: Could try to make Memcached::setMulti() work, though the return structure differs.
$result[ $key ] = $this->set( $key, $value, $group, $expire );
}
return $result;
}
/**
* Retrieves the cache contents, if it exists.
*
* @param int|string $key The key under which the cache contents are stored.
* @param string $group Optional. Where the cache contents are grouped. Default 'default'.
* @param bool $force Optional. Whether to force an update of the local cache
* from the persistent cache. Default false.
* @param bool $found Optional. Whether the key was found in the cache (passed by reference).
* Disambiguates a return of false, a storable value. Default null.
* @return mixed|false The cache contents on success, false on failure to retrieve contents.
*/
public function get( $key, $group = 'default', $force = false, &$found = null ) {
$key = $this->key( $key, $group );
if ( $force && $this->is_non_persistent_group( $group ) ) {
// There's nothing to "force" retrieve.
$force = false;
}
if ( isset( $this->cache[ $key ] ) && ! $force ) {
/** @psalm-suppress MixedAssignment */
$value = is_object( $this->cache[ $key ]['value'] ) ? clone $this->cache[ $key ]['value'] : $this->cache[ $key ]['value'];
$found = $this->cache[ $key ]['found'];
$this->group_ops_stats( 'get_local', $key, $group, null, null, 'local' );
return $value;
}
// For a non-persistant group, if it's not in local cache then it just doesn't exist.
if ( $this->is_non_persistent_group( $group ) ) {
$found = false;
$this->group_ops_stats( 'get_local', $key, $group, null, null, 'not_in_local' );
return false;
}
$this->timer_start();
$mc = $this->get_mc( $group );
/** @psalm-suppress MixedAssignment */
$value = $mc->get( $this->normalize_key( $key ) );
$found = \Memcached::RES_NOTFOUND !== $mc->getResultCode();
$elapsed = $this->timer_stop();
if ( 'checkthedatabaseplease' === $value ) {
unset( $this->cache[ $key ] );
$found = false;
$this->group_ops_stats( 'get', $key, $group, null, $elapsed, 'checkthedatabaseplease' );
return false;
}
$this->cache[ $key ] = [
'value' => $value,
'found' => $found,
];
if ( $found ) {
$this->group_ops_stats( 'get', $key, $group, $this->get_data_size( $value ), $elapsed, 'memcache' );
} else {
$this->group_ops_stats( 'get', $key, $group, null, $elapsed, 'not_in_memcache' );
}
return $value;
}
/**
* Retrieves multiple values from the cache in one call.
*
* @param array<string|int> $keys Array of keys under which the cache contents are stored.
* @param string $group Optional. Where the cache contents are grouped. Default 'default'.
* @param bool $force Optional. Whether to force an update of the local cache
* from the persistent cache. Default false.
* @return mixed[] Array of return values, grouped by key. Each value is either
* the cache contents on success, or false on failure.
*/
public function get_multiple( $keys, $group = 'default', $force = false ) {
$uncached_keys = [];
$return = [];
$return_cache = [];
if ( $force && $this->is_non_persistent_group( $group ) ) {
// There's nothing to "force" retrieve.
$force = false;
}
// First, fetch what we can from runtime cache.
foreach ( $keys as $key ) {
$cache_key = $this->key( $key, $group );
if ( isset( $this->cache[ $cache_key ] ) && ! $force ) {
/** @psalm-suppress MixedAssignment */
$return[ $key ] = is_object( $this->cache[ $cache_key ]['value'] ) ? clone $this->cache[ $cache_key ]['value'] : $this->cache[ $cache_key ]['value'];
$this->group_ops_stats( 'get_local', $cache_key, $group, null, null, 'local' );
} elseif ( $this->is_non_persistent_group( $group ) ) {
$return[ $key ] = false;
$this->group_ops_stats( 'get_local', $cache_key, $group, null, null, 'not_in_local' );
} else {
$uncached_keys[ $key ] = $cache_key;
}
}
if ( ! empty( $uncached_keys ) ) {
$mc = $this->get_mc( $group );
$keys_to_lookup = array_values( $uncached_keys ); // full cache keys
$mapped = $this->normalize_keys_with_mapping( $keys_to_lookup ); // normalized => full
$this->timer_start();
$raw = [];
if ( count( $mapped ) > 1000 ) {
foreach ( array_chunk( array_keys( $mapped ), 1000, true ) as $chunk ) {
/** @var array<string,mixed>|false $partial */
$partial = $mc->getMulti( $chunk );
if ( ! is_array( $partial ) ) {
// Match the adapter’s “bail on whole call” behavior if any batch fails.
$raw = false;
break;
}
$raw = array_merge( $raw, $partial );
}
} else {
/** @var array<string,mixed>|false $raw */
$raw = $mc->getMulti( array_keys( $mapped ) );
}
$elapsed = $this->timer_stop();
/** @var array<string, scalar|array|object|null> $values */
$values = []; // full_key => value
if ( is_array( $raw ) ) {
/** @var array<string, scalar|array|object|null> $raw */
foreach ( $raw as $normalized => $val ) {
$full = $mapped[ $normalized ] ?? $normalized;
$values[ $full ] = $val;
}
}
foreach ( $uncached_keys as $key => $cache_key ) {
$found = array_key_exists( $cache_key, $values );
/** @psalm-suppress MixedAssignment */
$value = $found ? $values[ $cache_key ] : false;
// Treat the remote invalidation special value as a miss and do not cache it locally.
if ( $found && 'checkthedatabaseplease' === $value ) {
$return[ $key ] = false;
// Ensure we do NOT persist a runtime entry for this key.
unset( $return_cache[ $cache_key ] );
continue;
}
/** @psalm-suppress MixedAssignment */
$return[ $key ] = $value;
$return_cache[ $cache_key ] = [
'value' => $value,
'found' => $found,
];
}
$this->group_ops_stats( 'get_multiple', array_values( $uncached_keys ), $group, $this->get_data_size( array_values( $values ) ), $elapsed );
}
$this->cache = array_merge( $this->cache, $return_cache );
return $return;
}
/**
* Retrieves multiple values from the cache in one call.
*
* @param array<string, array<string|int>> $groups Array of keys, indexed by group.
* Example: $groups['group-name'] = [ 'key1', 'key2' ]
*
* @return mixed[] Array of return values, grouped by key. Each value is either
* the cache contents on success, or false on failure.
*/
public function get_multi( $groups ) {
$return = [];
foreach ( $groups as $group => $keys ) {
$results = $this->get_multiple( $keys, $group );
foreach ( $keys as $key ) {
// This feels like a bug, as the full cache key is not useful to consumers. But alas, should be deprecating this method soon anyway.
$cache_key = $this->key( $key, $group );
/** @psalm-suppress MixedAssignment */
$return[ $cache_key ] = isset( $results[ $key ] ) ? $results[ $key ] : false;
}
}
return $return;
}
/**
* Removes the contents of the cache key in the group.
*
* @param int|string $key What the contents in the cache are called.
* @param string $group Optional. Where the cache contents are grouped. Default 'default'.
*
* @return bool True on success, false on failure or if the contents were not deleted.
*/
public function delete( $key, $group = 'default' ) {
$key = $this->key( $key, $group );
if ( $this->is_non_persistent_group( $group ) ) {
$result = isset( $this->cache[ $key ] );
unset( $this->cache[ $key ] );
return $result;
}
$this->timer_start();
$mc = $this->get_mc( $group );
$deleted = $mc->delete( $this->normalize_key( $key ) );
$elapsed = $this->timer_stop();
$this->group_ops_stats( 'delete', $key, $group, null, $elapsed );
// Remove from local cache regardless of the result.
unset( $this->cache[ $key ] );
return $deleted;
}
/**
* Deletes multiple values from the cache in one call.
*
* @param array<string|int> $keys Array of keys to be deleted.
* @param string $group Optional. Where the cache contents are grouped. Default empty.
* @return bool[] Array of return values, grouped by key. Each value is either
* true on success, or false if the contents were not deleted.
*/
public function delete_multiple( $keys, $group = '' ) {
if ( $this->is_non_persistent_group( $group ) ) {
$return = [];
foreach ( $keys as $key ) {
$cache_key = $this->key( $key, $group );
$deleted = isset( $this->cache[ $cache_key ] );
unset( $this->cache[ $cache_key ] );
$return[ $key ] = $deleted;
}
return $return;
}
$mapped_keys = $this->map_keys( $keys, $group ); // full_key => original_key
$this->timer_start();
$mc = $this->get_mc( $group );
// normalize full keys for memcached call
$normalized_map = $this->normalize_keys_with_mapping( array_keys( $mapped_keys ) ); // normalized => full
/** @psalm-var array<string, true|int> $results */
$results = $mc->deleteMulti( array_keys( $normalized_map ) );
$elapsed = $this->timer_stop();
$this->group_ops_stats( 'delete_multiple', array_keys( $mapped_keys ), $group, null, $elapsed );
$return = [];
foreach ( $results as $norm_key => $result_value ) {
$full_key = $normalized_map[ $norm_key ] ?? $norm_key;
$original_key = $mapped_keys[ $full_key ] ?? $full_key;
// deleteMulti returns true on success, or Memcached::RES_* int on failure
$return[ $original_key ] = ( true === $result_value );
unset( $this->cache[ $full_key ] );
}
return $return;
}
/**
* Increments numeric cache item's value.
*
* @param int|string $key The cache key to increment.
* @param int $offset Optional. The amount by which to increment the item's value.
* Default 1.
* @param string $group Optional. The group the key is in. Default 'default'.
* @return int|false The item's new value on success, false on failure.
*/
public function incr( $key, $offset = 1, $group = 'default' ) {
$key = $this->key( $key, $group );
if ( $this->is_non_persistent_group( $group ) ) {
if ( ! isset( $this->cache[ $key ] ) || ! is_int( $this->cache[ $key ]['value'] ) ) {
return false;
}
$this->cache[ $key ]['value'] += $offset;
return $this->cache[ $key ]['value'];
}
$this->timer_start();
$mc = $this->get_mc( $group );
$incremented = $mc->increment( $this->normalize_key( $key ), $offset );
$elapsed = $this->timer_stop();
$this->group_ops_stats( 'increment', $key, $group, null, $elapsed );
$this->cache[ $key ] = [
'value' => $incremented,
'found' => false !== $incremented,
];
return $incremented;
}
/**
* Decrements numeric cache item's value.
*
* @param int|string $key The cache key to decrement.
* @param int $offset Optional. The amount by which to decrement the item's value.
* Default 1.
* @param string $group Optional. The group the key is in. Default 'default'.
* @return int|false The item's new value on success, false on failure.
*/
public function decr( $key, $offset = 1, $group = 'default' ) {
$key = $this->key( $key, $group );
if ( $this->is_non_persistent_group( $group ) ) {
if ( ! isset( $this->cache[ $key ] ) || ! is_int( $this->cache[ $key ]['value'] ) ) {
return false;
}
$new_value = $this->cache[ $key ]['value'] - $offset;
if ( $new_value < 0 ) {
$new_value = 0;
}
$this->cache[ $key ]['value'] = $new_value;
return $this->cache[ $key ]['value'];
}
$this->timer_start();
$mc = $this->get_mc( $group );
$decremented = $mc->decrement( $this->normalize_key( $key ), $offset );
$elapsed = $this->timer_stop();
$this->group_ops_stats( 'decrement', $key, $group, null, $elapsed );
$this->cache[ $key ] = [
'value' => $decremented,
'found' => false !== $decremented,
];
return $decremented;
}
/**
* Clears the object cache of all data.
*
* Purposely does not use the memcached flush method,
* as that acts on the entire memcached server, affecting all sites.
* Instead, we rotate the key prefix for the current site,
* along with the global key when flushing the main site.
*
* @return true Always returns true.
*/
public function flush() {
$this->cache = [];
$flush_number = $this->new_flush_number();
$this->rotate_site_keys( $flush_number );
if ( is_main_site() ) {
$this->rotate_global_keys( $flush_number );
}
return true;
}
/**
* Unsupported: Removes all cache items in a group.
*
* @param string $_group Name of group to remove from cache.
* @return bool Returns false, as there is no support for group flushes.
*/
public function flush_group( $_group ) {
return false;
}
/**
* Removes all cache items from the in-memory runtime cache.
* Also reset the local stat-related tracking for individual operations.
*
* @return true Always returns true.
*/
public function flush_runtime() {
$this->cache = [];
$this->group_ops = [];
return true;
}
/**
* Sets the list of global cache groups.
*
* @param string|string[] $groups List of groups that are global.
* @return void
*/
public function add_global_groups( $groups ) {
if ( ! is_array( $groups ) ) {
$groups = (array) $groups;
}
$this->global_groups = array_unique( array_merge( $this->global_groups, $groups ) );
}
/**
* Sets the list of non-persistent groups.
*
* @param string|string[] $groups List of groups that will not be saved to persistent cache.
* @return void
*/
public function add_non_persistent_groups( $groups ) {
if ( ! is_array( $groups ) ) {
$groups = (array) $groups;
}
$this->no_mc_groups = array_unique( array_merge( $this->no_mc_groups, $groups ) );
}
/**
* Switches the internal blog ID.
*
* This changes the blog ID used to create keys in blog specific groups.
*
* @param int $blog_id Blog ID.
* @return void
*/
public function switch_to_blog( $blog_id ) {
global $table_prefix;
/** @psalm-suppress RedundantCastGivenDocblockType **/
$blog_id = (int) $blog_id;
$this->blog_prefix = (string) ( is_multisite() ? $blog_id : $table_prefix );
}
/**
* Close the connections.
*
* @return bool
*/
public function close() {
// Memcached::quit() closes persistent connections, which we don't want to do.
return true;
}
/*
|--------------------------------------------------------------------------
| Internal methods that deal with flush numbers, the pseudo-cache-flushing mechanic.
| Public methods here may be deprecated & made private in the future.
|--------------------------------------------------------------------------
*/
/**
* Get the flush number prefix, used for creating the key string.
*
* @param string|int $group
* @return string
*/
public function flush_prefix( $group ) {
if ( $group === $this->flush_group || $group === $this->global_flush_group ) {
// Never flush the flush numbers.
$number = '_';
} elseif ( false !== array_search( $group, $this->global_groups ) ) {
$number = $this->get_global_flush_number();
} else {
$number = $this->get_blog_flush_number();
}
return $number . ':';
}
/**
* Get the global group flush number.
*
* @return int
*/
public function get_global_flush_number() {
if ( ! isset( $this->global_flush_number ) ) {
$this->global_flush_number = $this->get_flush_number( $this->global_flush_group );
}
return $this->global_flush_number;
}
/**
* Get the blog's flush number.
*
* @return int
*/
public function get_blog_flush_number() {
if ( ! isset( $this->flush_number[ $this->blog_prefix ] ) ) {
$this->flush_number[ $this->blog_prefix ] = $this->get_flush_number( $this->flush_group );
}
return $this->flush_number[ $this->blog_prefix ];
}
/**
* Get the flush number for a specific group.
*
* @param string $group
* @return int
*/
public function get_flush_number( $group ) {
$flush_number = $this->get_max_flush_number( $group );
if ( empty( $flush_number ) ) {
// If there was no flush number anywhere, make a new one. This flushes the cache.
$flush_number = $this->new_flush_number();
$this->set_flush_number( $flush_number, $group );
}
return $flush_number;
}
/**
* Set the flush number for a specific group.
*
* @param int $value
* @param string $group
* @return void
*/
public function set_flush_number( $value, $group ) {
$key = $this->key( $this->flush_key, $group );
$expire = 0;
$size = 19; // size of the microsecond timestamp serialized
$this->timer_start();
$mc = $this->mc['default'] ?? null;
if ( $mc ) {
foreach ( $this->redundancy_server_keys as $server_string => $server_key ) {
$mc->setByKey( $server_key, $key, $value, $expire );
}
}
$elapsed = $this->timer_stop();
$replication_servers_count = max( count( $this->default_mcs ), 1 );
$average_time_elapsed = $elapsed / (float) $replication_servers_count;
foreach ( $this->default_mcs as $_default_mc ) {
$this->group_ops_stats( 'set_flush_number', $key, $group, $size, $average_time_elapsed, 'replication' );
}
}
/**
* Get the highest flush number from all default servers, replicating if needed.
*
* @param string $group
* @return int|false
*/
public function get_max_flush_number( $group ) {
$key = $this->key( $this->flush_key, $group );
$size = 19; // size of the microsecond timestamp serialized
$this->timer_start();
$values = [];
$mc = $this->mc['default'] ?? null;
if ( $mc ) {
foreach ( $this->redundancy_server_keys as $server_string => $server_key ) {
/** @psalm-suppress MixedAssignment */
$values[ $server_string ] = $mc->getByKey( $server_key, $key );
}
}
$elapsed = $this->timer_stop();
$replication_servers_count = max( count( $this->default_mcs ), 1 );
$average_time_elapsed = $elapsed / (float) $replication_servers_count;
/** @psalm-suppress MixedAssignment */
foreach ( $values as $result ) {
if ( false === $result ) {
$this->group_ops_stats( 'get_flush_number', $key, $group, null, $average_time_elapsed, 'not_in_memcache' );
} else {
$this->group_ops_stats( 'get_flush_number', $key, $group, $size, $average_time_elapsed, 'memcache' );
}
}
$values = array_map( 'intval', $values );
/** @psalm-suppress ArgumentTypeCoercion */
$max = max( $values );
if ( $max <= 0 ) {
return false;
}
$servers_to_update = [];
foreach ( $values as $server_string => $value ) {
if ( $value < $max ) {
$servers_to_update[] = $server_string;
}
}
// Replicate to servers not having the max.
if ( ! empty( $servers_to_update ) ) {
$expire = 0;
$this->timer_start();
if ( $mc ) {
foreach ( $this->redundancy_server_keys as $server_string => $server_key ) {
if ( in_array( $server_string, $servers_to_update, true ) ) {
$mc->setByKey( $server_key, $key, $max, $expire );
}
}
}
$elapsed = $this->timer_stop();
$average_time_elapsed = $elapsed / (float) count( $servers_to_update );
foreach ( $servers_to_update as $updated_server ) {
$this->group_ops_stats( 'set_flush_number', $key, $group, $size, $average_time_elapsed, 'replication_repair' );
}
}
return $max;
}
/**
* Rotate the flush number for the site/blog.
*
* @param ?int $flush_number
* @return void
*/
public function rotate_site_keys( $flush_number = null ) {
if ( is_null( $flush_number ) ) {
$flush_number = $this->new_flush_number();
}
$this->set_flush_number( $flush_number, $this->flush_group );
$this->flush_number[ $this->blog_prefix ] = $flush_number;
}
/**
* Rotate the global flush number.
*
* @param ?int $flush_number
* @return void
*/
public function rotate_global_keys( $flush_number = null ) {
if ( is_null( $flush_number ) ) {
$flush_number = $this->new_flush_number();
}
$this->set_flush_number( $flush_number, $this->global_flush_group );
$this->global_flush_number = $flush_number;
}
/**
* Generate new flush number.
*
* @return int
*/
public function new_flush_number(): int {
return intval( microtime( true ) * 1e6 );
}
/*
|--------------------------------------------------------------------------
| Utility methods. Internal use only.
| Public methods here may be deprecated & made private in the future.
|--------------------------------------------------------------------------
*/
/**
* Generate the key we'll use to interact with memcached.
* Note: APCU requires this to be public right now.
*
* @param int|string $key
* @param int|string $group
* @return string
*/
public function key( $key, $group ): string {
if ( empty( $group ) ) {
$group = 'default';
}
$result = sprintf(
'%s%s%s:%s:%s',
$this->key_salt,
$this->flush_prefix( $group ),
array_search( $group, $this->global_groups ) !== false ? $this->global_prefix : $this->blog_prefix,
$group,
$key
);
return (string) preg_replace( '/\\s+/', '', $result );
}
/**
* Map the full cache key to the original key.
*
* @param array<int|string> $keys
* @param string $group
* @return array<string, int|string>
*/
protected function map_keys( $keys, $group ): array {
$results = [];
foreach ( $keys as $key ) {
$results[ $this->key( $key, $group ) ] = $key;
}
return $results;
}
/**
* Get the memcached instance for the specified group.
*
* @param int|string $group
* @return Memcached
*/
public function get_mc( $group ) {
if ( isset( $this->mc[ $group ] ) ) {
return $this->mc[ $group ];
}
return $this->mc['default'];
}
/**
* Sanitize the expiration time.
*
* @psalm-param int|numeric-string|float $expire
*/
private function get_expiration( $expire ): int {
$expire = intval( $expire );
if ( $expire <= 0 || $expire > $this->max_expiration ) {
$expire = $this->default_expiration;
}
return $expire;
}
/**
* Check if the group is set up for non-persistent cache.
*
* @param string|int $group
* @return bool
*/
private function is_non_persistent_group( $group ) {
return in_array( $group, $this->no_mc_groups, true );
}
/**
* Estimate the (uncompressed) data size.
*
* @param mixed $data
* @return int
* @psalm-return 0|positive-int
*/
public function get_data_size( $data ): int {
if ( is_string( $data ) ) {
return strlen( $data );
}
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
return strlen( serialize( $data ) );
}
/**
* Sets the key salt property.
*
* @param mixed $key_salt
* @param bool $add_mc_prefix
* @return void
*/
public function salt_keys( $key_salt, $add_mc_prefix = false ) {
$key_salt = is_string( $key_salt ) && strlen( $key_salt ) ? $key_salt : '';
$key_salt = $add_mc_prefix ? $key_salt . '_mc' : $key_salt;
$this->key_salt = empty( $key_salt ) ? '' : $key_salt . ':';
}
public function timer_start(): bool {
$this->time_start = microtime( true );
return true;
}
public function timer_stop(): float {
return microtime( true ) - $this->time_start;
}
/**
* TODO: Deprecate.
*
* @param string $host
* @param string $port
*/
public function failure_callback( $host, $port ): void {
$this->connection_errors[] = array(
'host' => $host,
'port' => $port,
);
}
/*
|--------------------------------------------------------------------------
| Memcached Internals (Inlined from adapter)
|--------------------------------------------------------------------------
*/
/**
* Servers and configurations are persisted between requests.
* Only add servers when configuration changed.
*
* @param string $name
* @psalm-param array<int, array{host: string, port: int, weight: int}> $servers
* @return \Memcached
*/
private function create_connection_pool( string $name, array $servers ): \Memcached {
$mc = new \Memcached( $name );
// Servers and configurations are persisted between requests.
/** @psalm-var array<int,array{host: string, port: int, type: string}> $existing_servers */
$existing_servers = $mc->getServerList();
// Check if the servers have changed since they were registered.
$needs_refresh = count( $existing_servers ) !== count( $servers );
foreach ( $servers as $index => $server ) {
$existing_host = $existing_servers[ $index ]['host'] ?? null;
$existing_port = $existing_servers[ $index ]['port'] ?? null;
if ( $existing_host !== $server['host'] || $existing_port !== $server['port'] ) {
$needs_refresh = true;
}
}
if ( $needs_refresh ) {
$mc->resetServerList();
$servers_to_add = [];
foreach ( $servers as $server ) {
$servers_to_add[] = [ $server['host'], $server['port'], $server['weight'] ];
}
$mc->addServers( $servers_to_add );
$mc->setOptions( $this->get_config_options() );
}
return $mc;
}
/**
* @param string $address
* @psalm-return array{host: string, port: int}
*/
private function parse_address( string $address ): array {
$default_port = 11211;
if ( 'unix://' === substr( $address, 0, 7 ) ) {
// Memcached wants unix:// stripped.
$host = substr( $address, 7 );
$port = 0;
} else {
$items = explode( ':', $address, 2 );
$host = $items[0];
$port = isset( $items[1] ) ? intval( $items[1] ) : $default_port;
}
return [
'host' => $host,
'port' => $port,
];
}
private function get_config_options(): array {
/**
* @psalm-suppress RedundantCondition
* @psalm-suppress TypeDoesNotContainType
*/
$serializer = \Memcached::HAVE_IGBINARY && extension_loaded( 'igbinary' )
? \Memcached::SERIALIZER_IGBINARY
: \Memcached::SERIALIZER_PHP;
return [
\Memcached::OPT_BINARY_PROTOCOL => false,
\Memcached::OPT_SERIALIZER => $serializer,
\Memcached::OPT_CONNECT_TIMEOUT => 1000,
\Memcached::OPT_COMPRESSION => true,
\Memcached::OPT_TCP_NODELAY => true,
];
}
/**
* We want a unique string that maps to each default server so we can use setByKey/getByKey
* to talk to servers individually (for flush number redundancy).
*
* @psalm-return array<string, string> // "host:port" => server_key
*/
private function get_server_keys_for_redundancy(): array {
if ( ! isset( $this->mc['default'] ) ) {
return [];
}
$default_pool = $this->mc['default'];
$servers = $default_pool->getServerList();
$server_keys = [];
for ( $i = 0; $i < 1000; $i++ ) {
$test_key = 'redundancy_key_' . $i;
/** @psalm-var array{host: string, port: int, weight: int}|false $result */
$result = $default_pool->getServerByKey( $test_key );
if ( ! $result ) {
continue;
}
$server_string = $result['host'] . ':' . $result['port'];
if ( ! isset( $server_keys[ $server_string ] ) ) {
$server_keys[ $server_string ] = $test_key;
}
// Keep going until every server is accounted for (capped at 1000 attempts - which is incredibly unlikely unless there are tons of servers).
if ( count( $server_keys ) === count( $servers ) ) {
break;
}
}
return $server_keys;
}
/**
* Memcached strictly enforces key length <= 250.
* We shorten long keys deterministically.
*
* @psalm-return string
*/
private function normalize_key( string $key ): string {
if ( strlen( $key ) <= 250 ) {
return $key;
}
return substr( $key, 0, 200 ) . ':truncated:' . md5( $key );
}
/**
* Reduce key lengths while keeping a map normalized_key => original_key.
*
* @param string[] $keys
* @psalm-return array<string,string>
*/
private function normalize_keys_with_mapping( array $keys ): array {
$mapped = [];
foreach ( $keys as $key ) {
$mapped[ $this->normalize_key( $key ) ] = $key;
}
return $mapped;
}
/*
|--------------------------------------------------------------------------
| Stat-related tracking & output.
| A lot of the below should be deprecated/removed in the future.
|--------------------------------------------------------------------------
*/
/**
* Echoes the stats of the caching operations that have taken place.
* Ideally this should be the only method left public in this section.
*
* @return void Outputs the info directly, nothing is returned.
*/
public function stats() {
$this->stats_helper->stats();
}
/**
* Sets the key salt property.
*
* @param string $op The operation taking place, such as "set" or "get".
* @param string|string[] $keys The memcached key/keys involved in the operation.
* @param string $group The group the keys are in.
* @param ?int $size The size of the data invovled in the operation.
* @param ?float $time The time the operation took.
* @param string $comment Extra notes about the operation.
*
* @return void
*/
public function group_ops_stats( $op, $keys, $group, $size = null, $time = null, $comment = '' ) {
$this->stats_helper->group_ops_stats( $op, $keys, $group, $size, $time, $comment );
}
/**
* Returns the collected raw stats.
*/
public function get_stats(): array {
return $this->stats_helper->get_stats();
}
/**
* @param string $field The stat field/group being incremented.
* @param int $num Amount to increment by.
*/
public function increment_stat( $field, $num = 1 ): void {
$this->stats_helper->increment_stat( $field, $num );
}
/**
* @param string|string[] $keys
* @return string|string[]
*/
public function strip_memcached_keys( $keys ) {
return $this->stats_helper->strip_memcached_keys( $keys );
}
public function js_toggle(): void {
$this->stats_helper->js_toggle();
}
/**
* @param string $line
* @param string $trailing_html
* @return string
*/
public function colorize_debug_line( $line, $trailing_html = '' ) {
return $this->stats_helper->colorize_debug_line( $line, $trailing_html );
}
/**
* @param string|int $index
* @param array $arr
* @psalm-param array{0: string, 1: string|string[], 2: int|null, 3: float|null, 4: string, 5: string, 6: string|null } $arr
*
* @return string
*/
public function get_group_ops_line( $index, $arr ) {
return $this->stats_helper->get_group_ops_line( $index, $arr );
}
}
/**
* Sets up Object Cache Global and assigns it.
*
* @global WP_Object_Cache $wp_object_cache
* @return void
*/
function wp_cache_init() {
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$GLOBALS['wp_object_cache'] = new WP_Object_Cache();
}
/**
* Adds data to the cache, if the cache key doesn't already exist.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key The cache key to use for retrieval later.
* @param mixed $data The data to add to the cache.
* @param string $group Optional. The group to add the cache to. Enables the same key
* to be used across groups. Default empty.
* @param int $expire Optional. When the cache data should expire, in seconds.
* Default 0 (no expiration).
* @return bool True on success, false if cache key and group already exist.
*/
function wp_cache_add( $key, $data, $group = '', $expire = 0 ) {
global $wp_object_cache;
// Falsey values for alloptions should never be cached.
if ( 'alloptions' === $key && 'options' === $group && ! $data ) {
return false;
}
/** @psalm-suppress RedundantCastGivenDocblockType */
return $wp_object_cache->add( $key, $data, $group, (int) $expire );
}
/**
* Adds multiple values to the cache in one call.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param array $data Array of keys and values to be set.
* @param string $group Optional. Where the cache contents are grouped. Default empty.
* @param int $expire Optional. When to expire the cache contents, in seconds.
* Default 0 (no expiration).
* @return bool[] Array of return values, grouped by key. Each value is either
* true on success, or false if cache key and group already exist.
* @psalm-param mixed[] $data
*/
function wp_cache_add_multiple( array $data, $group = '', $expire = 0 ) {
global $wp_object_cache;
/** @psalm-suppress RedundantCastGivenDocblockType */
return $wp_object_cache->add_multiple( $data, $group, (int) $expire );
}
/**
* Replaces the contents of the cache with new data.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key The key for the cache data that should be replaced.
* @param mixed $data The new data to store in the cache.
* @param string $group Optional. The group for the cache data that should be replaced.
* Default empty.
* @param int $expire Optional. When to expire the cache contents, in seconds.
* Default 0 (no expiration).
* @return bool True if contents were replaced, false if original value does not exist.
* @psalm-suppress RedundantCast
*/
function wp_cache_replace( $key, $data, $group = '', $expire = 0 ) {
global $wp_object_cache;
return $wp_object_cache->replace( $key, $data, $group, (int) $expire );
}
/**
* Saves the data to the cache.
*
* Differs from wp_cache_add() and wp_cache_replace() in that it will always write data.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key The cache key to use for retrieval later.
* @param mixed $data The contents to store in the cache.
* @param string $group Optional. Where to group the cache contents. Enables the same key
* to be used across groups. Default empty.
* @param int $expire Optional. When to expire the cache contents, in seconds.
* Default 0 (no expiration).
* @return bool True on success, false on failure.
*/
function wp_cache_set( $key, $data, $group = '', $expire = 0 ) {
global $wp_object_cache;
if ( ! defined( 'WP_INSTALLING' ) ) {
/** @psalm-suppress RedundantCastGivenDocblockType */
return $wp_object_cache->set( $key, $data, $group, (int) $expire );
}
return $wp_object_cache->delete( $key, $group );
}
/**
* Sets multiple values to the cache in one call.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param array $data Array of keys and values to be set.
* @param string $group Optional. Where the cache contents are grouped. Default empty.
* @param int $expire Optional. When to expire the cache contents, in seconds.
* Default 0 (no expiration).
* @return bool[] Array of return values, grouped by key. Each value is either
* true on success, or false on failure.
* @psalm-param mixed[] $data
*/
function wp_cache_set_multiple( array $data, $group = '', $expire = 0 ) {
global $wp_object_cache;
if ( ! defined( 'WP_INSTALLING' ) ) {
/** @psalm-suppress RedundantCastGivenDocblockType */
return $wp_object_cache->set_multiple( $data, $group, (int) $expire );
}
return $wp_object_cache->delete_multiple( array_keys( $data ), $group );
}
/**
* Retrieves the cache contents from the cache by key and group.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key The key under which the cache contents are stored.
* @param string $group Optional. Where the cache contents are grouped. Default empty.
* @param bool $force Optional. Whether to force an update of the local cache
* from the persistent cache. Default false.
* @param bool $found Optional. Whether the key was found in the cache (passed by reference).
* Disambiguates a return of false, a storable value. Default null.
* @return mixed|false The cache contents on success, false on failure to retrieve contents.
*/
function wp_cache_get( $key, $group = '', $force = false, &$found = null ) {
global $wp_object_cache;
$value = apply_filters( 'pre_wp_cache_get', false, $key, $group, $force );
if ( false !== $value ) {
$found = true;
return $value;
}
return $wp_object_cache->get( $key, $group, $force, $found );
}
/**
* Retrieves multiple values from the cache in one call.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param array $keys Array of keys under which the cache contents are stored.
* @param string $group Optional. Where the cache contents are grouped. Default empty.
* @param bool $force Optional. Whether to force an update of the local cache
* from the persistent cache. Default false.
* @return array Array of return values, grouped by key. Each value is either
* the cache contents on success, or false on failure.
* @psalm-param (int|string)[] $keys
* @psalm-return mixed[]
*/
function wp_cache_get_multiple( $keys, $group = '', $force = false ) {
global $wp_object_cache;
return $wp_object_cache->get_multiple( $keys, $group, $force );
}
/**
* Retrieves multiple values from the cache in one call.
* TODO: Deprecate
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param array<string, array<string|int>> $groups Array of keys, indexed by group. Example: $groups['group-name'] = [ 'key1', 'key2' ]
*
* @return array Array of return values, with the full cache key as the index. Each value is either the cache contents on success, or false on failure.
*/
function wp_cache_get_multi( $groups ) {
global $wp_object_cache;
return $wp_object_cache->get_multi( $groups );
}
/**
* Removes the cache contents matching key and group.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key What the contents in the cache are called.
* @param string $group Optional. Where the cache contents are grouped. Default empty.
* @return bool True on successful removal, false on failure.
*/
function wp_cache_delete( $key, $group = '' ) {
global $wp_object_cache;
return $wp_object_cache->delete( $key, $group );
}
/**
* Deletes multiple values from the cache in one call.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param array $keys Array of keys under which the cache to deleted.
* @param string $group Optional. Where the cache contents are grouped. Default empty.
* @return bool[] Array of return values, grouped by key. Each value is either
* true on success, or false if the contents were not deleted.
* @psalm-param (int|string)[] $keys
*/
function wp_cache_delete_multiple( array $keys, $group = '' ) {
global $wp_object_cache;
return $wp_object_cache->delete_multiple( $keys, $group );
}
/**
* Increments numeric cache item's value.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key The key for the cache contents that should be incremented.
* @param int $offset Optional. The amount by which to increment the item's value. Default 1.
* @param string $group Optional. The group the key is in. Default empty.
* @return int|false The item's new value on success, false on failure.
*/
function wp_cache_incr( $key, $offset = 1, $group = '' ) {
global $wp_object_cache;
/** @psalm-suppress RedundantCastGivenDocblockType */
return $wp_object_cache->incr( $key, (int) $offset, $group );
}
/**
* Decrements numeric cache item's value.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key The cache key to decrement.
* @param int $offset Optional. The amount by which to decrement the item's value. Default 1.
* @param string $group Optional. The group the key is in. Default empty.
* @return int|false The item's new value on success, false on failure.
*/
function wp_cache_decr( $key, $offset = 1, $group = '' ) {
global $wp_object_cache;
/** @psalm-suppress RedundantCastGivenDocblockType */
return $wp_object_cache->decr( $key, (int) $offset, $group );
}
/**
* Removes all cache items.
*
* @see WP_Object_Cache::flush()
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @return bool True on success, false on failure.
*/
function wp_cache_flush() {
global $wp_object_cache;
return $wp_object_cache->flush();
}
/**
* Removes all cache items from the in-memory runtime cache.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @return bool True on success, false on failure.
*/
function wp_cache_flush_runtime() {
global $wp_object_cache;
return $wp_object_cache->flush_runtime();
}
/**
* Unsupported: Removes all cache items in a group, if the object cache implementation supports it.
*
* Before calling this function, always check for group flushing support using the
* `wp_cache_supports( 'flush_group' )` function.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param string $_group Name of group to remove from cache.
* @return bool True if group was flushed, false otherwise.
*/
function wp_cache_flush_group( $_group ) {
global $wp_object_cache;
return $wp_object_cache->flush_group( $_group );
}
/**
* Determines whether the object cache implementation supports a particular feature.
*
* @param string $feature Name of the feature to check for. Possible values include:
* 'add_multiple', 'set_multiple', 'get_multiple', 'delete_multiple',
* 'flush_runtime', 'flush_group'.
* @return bool True if the feature is supported, false otherwise.
*/
function wp_cache_supports( $feature ) {
switch ( $feature ) {
case 'add_multiple':
case 'set_multiple':
case 'get_multiple':
case 'delete_multiple':
case 'flush_runtime':
return true;
default:
return false;
}
}
/**
* Closes the cache.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @return bool
*/
function wp_cache_close() {
global $wp_object_cache;
return $wp_object_cache->close();
}
/**
* Adds a group or set of groups to the list of global groups.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param string|string[] $groups A group or an array of groups to add.
* @return void
*/
function wp_cache_add_global_groups( $groups ) {
global $wp_object_cache;
$wp_object_cache->add_global_groups( $groups );
}
/**
* Adds a group or set of groups to the list of non-persistent groups.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param string|string[] $groups A group or an array of groups to add.
* @return void
*/
function wp_cache_add_non_persistent_groups( $groups ) {
global $wp_object_cache;
$wp_object_cache->add_non_persistent_groups( $groups );
}
/**
* Switches the internal blog ID.
* This changes the blog id used to create keys in blog specific groups.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int $blog_id Site ID.
* @return void
*/
function wp_cache_switch_to_blog( $blog_id ) {
global $wp_object_cache;
/** @psalm-suppress RedundantCastGivenDocblockType */
$wp_object_cache->switch_to_blog( (int) $blog_id );
}