Berikut adalah evaluasi mendalam mengenai fitur-fitur dan kelebihan dari script object-cache.php (varian Memcached v5.8.1) :
1. Arsitektur L1/L2 Cache
Script ini mengimplementasikan arsitektur cache dua tingkat:
- L1 (Local Runtime Cache): Menyimpan data di dalam memori proses PHP saat satu permintaan (request) berlangsung. Ini sangat efisien untuk mengurangi round-trip jaringan ke server Memcached dalam satu siklus hidup halaman.
- L2 (Memcached): Menyimpan data persisten di server Memcached yang bisa diakses lintas proses dan request.
- Eviction System: Terdapat batas maksimal item di L1 (
AOC_LOCAL_CACHE_LIMIT = 70000) dan sistem pembersihan (eviction) yang akan membuang item tertua jika batas tercapai, mencegah kebocoran memori pada PHP.
2. Native Reference Semantics (Tanpa Cloning)
- Kelebihan: Pada versi 5.8.1, fitur object cloning dihapus. WordPress secara native mengembalikan referensi objek dari cache, bukan salinannya. Dengan menghapus proses clone, script ini menghemat overhead CPU sebesar 5-20ms per cache hit, sekaligus menjaga kompatibilitas dengan perilaku asli WordPress (native reference semantics).
3. Manajemen Invalidation yang Canggih (Namespace & Group Versioning)
Karena Memcached tidak mendukung penghapusan key berdasarkan pola/wildcard (seperti SCAN atau DEL prefix* di Redis), script ini menggunakan sistem versi:
- Namespace Versioning (
bump_ns): Digunakan saatflush()dipanggil. Alih-alih menghapus jutaan key, sistem hanya menaikkan versi namespace. Key lama secara otomatis tidak akan terbaca karena prefix key berubah. - Group Versioning (
bump_group): Digunakan saatflush_group($group)dipanggil. Sistem menaikkan versi pada group tertentu sehingga key di dalam group tersebut otomatis “kadaluarsa” tanpa harus dihapus satu per satu dari Memcached. - Pending Bumps & Replay: Jika Memcached sedang down saat
flushatauflush_groupdipanggil, permintaan bump ini dimasukkan ke antrian (pending_ns_bumps/pending_group_bumps) dan akan dieksekusi ulang begitu koneksi Memcached pulih.
4. Fail-Open & Auto-Recovery
- Fail-Open Mechanism: Konfigurasi
AOC_FAIL_OPEN_WRITEdanAOC_FAIL_OPEN_ADDmemungkinkan cache tetap berfungsi secara lokal (di L1) meskipun server Memcached sedang mati. Ini mencegah situs mengalami downtime total (Error 500) saat cache backend gagal. - Auto-Recovery: Terdapat metode
check_memcached_recovery()yang dijalankan berkala (berdasarkan TTL failover). Jika Memcached awalnya down lalu kembali online, koneksi akan otomatis direstart, cache L1 dibersihkan untuk mencegah orphaned keys, dan antrian bump dieksekusi kembali.
5. Optimasi Operasi Batch (Chunking)
- Mendukung fungsi batch WordPress modern seperti
get_multiple,set_multiple,delete_multiple, danadd_multiple. - Pipelining & Chunking: Operasi batch dipecah menjadi chunk berukuran
AOC_BATCH_CHUNK(default 500). Operasi ini memanfaatkan native method Memcached sepertigetMulti(),setMulti(), dandeleteMulti(), mengurangi latensi jaringan secara dramatis saat memproses ratusan data sekaligus (misalnya saat memuat options atau post meta).
6. Keamanan & Sanitasi Key
- Key Hashing: Menggunakan algoritma
xxh3(sangat cepat) jika tersedia, atau fallback kesha1untuk menangani karakter ilegal atau key yang melebihi batas panjang maksimal Memcached (AOC_MAX_KEY_LENGTH = 230). - False Value Handling: Memcached secara default mengembalikan
falsejika key tidak ditemukan. Script ini menggunakanFALSE_TOKEN(\0__AOC_FALSE__\0) untuk membedakan antara nilai aslifalseyang di-cache dan cache miss. - Resource Blocking: Melindungi L1 cache agar tidak menyimpan tipe data
resourceyang tidak bisa di-serialize, mencegah error fatal.
7. Modern PHP & Strict Typing
- Menggunakan
declare(strict_types=1)dan mensyaratkan PHP 8.1+. - Memanfaatkan fitur modern seperti
readonlyproperties, union types (int|false),matchexpression, dan null-safe operator. Ini membuat kode lebih bersih, aman, dan dieksekusi lebih cepat oleh engine PHP modern.
8. Integrasi WooCommerce & Privasi Pengguna
- WooCommerce Strict Mode (
AOC_WOO_MODE): Secara otomatis mendeteksi key WooCommerce (seperti cart hash, session) dan memindahkannya ke Non-Persistent (NP) groups. Ini sangat penting karena data WooCommerce bersifat dinamis dan tidak boleh tersangkut di cache persisten yang lambat diperbarui. - User Privacy (
AOC_STRICT_USER_PRIVACY): Jika diaktifkan, data sensitif pengguna (sepertiusers,usermeta,session_tokens) akan otomatis di-set sebagai non-persistent untuk mencegah kebocoran data antar sesi atau antar pengguna.
Catatan Keterbatasan (Limitations)
Sebagaimana disebutkan dalam komentar header script:
- Race Conditions pada Counter: Memcached tidak mendukung Lua scripting seperti Redis. Operasi increment pada namespace atau group versioning memiliki race window kecil pada trafik sangat tinggi (concurrency).
- Best-Effort Bumps: Jika terjadi kegagalan jaringan saat menaikkan versi, sistem berusaha sebaik mungkin meng-queue dan mengulanginya, namun tetap lebih rentan dibandingkan implementasi Redis yang memiliki atomic operations.
Kesimpulan
Script object-cache.php ini adalah drop-in object cache tingkat “Enterprise” (God Tier) untuk WordPress yang memanfaatkan Memcached. Sangat cocok untuk situs web dengan trafik tinggi, situs multisite, atau WooCommerce. Ia menutupi kelemahan dasar Memcached (seperti ketiadaan wildcard delete) melalui sistem versioning, dan mengoptimalkan performa via batching, L1 caching, dan native reference semantics.
<?php
declare(strict_types=1);
/**
* Memcached Object Cache – Stripped (v5.8.1 Reference Semantics & Optimizations)
*
* Core Engine: v5.8.1 (Removed clone for WP reference semantics, optimized build_key & local eviction).
* Ported and synced with Redis v5.7.39 God Tier Build.
*
* LIMITATIONS vs Redis:
* – No atomic operations (Memcached lacks Lua scripting)
* – Race conditions possible in counter increments during high concurrency
* – Best-effort pending bumps (same as Redis, but with higher race window)
*
* @package AOC
* @author irawan.office + Senior Audit
* @version 5.8.1-memcached
*
* Plugin Name: Memcached Object Cache – Stripped (memcached_only)
* Description: Drop-in minimal Memcached object cache. Ultra-clean L1/L2 coherence. High-Traffic Optimized Edition.
* Version: 5.8.1-memcached
* Author: irawan.office (stripped + patched + final audit + JIT micro optimized)
* Requires PHP: 8.1
*
* Changelog:
* 5.8.1-memcached – [CRITICAL] Removed object cloning — restored WordPress native reference semantics for cache hits.
* 5.8.1-memcached – [PERF] Eliminated 5-20ms overhead per cache hit from unnecessary object cloning.
* 5.8.1-memcached – [PERF] Removed redundant checks in build_key() for NP group detection.
* 5.8.1-memcached – [HARDEN] Prevented resources from entering L1 cache.
* 5.8.1-memcached – [HYGIENE] Rate-limit bump failure logs to prevent error_log flooding during outages.
* 5.8.0-memcached – [PORT] Initial Memcached port from Redis v5.7.32 God Tier Build.
*/
defined(“ABSPATH”) || exit();
if (\PHP_VERSION_ID < 80100) {
if (defined(“WP_DEBUG”) && WP_DEBUG && defined(“WP_DEBUG_LOG”) && WP_DEBUG_LOG) {
\error_log(“[AOC Memcached] PHP 8.1+ required. Falling back to WordPress default object cache.”);
}
require_once ABSPATH . WPINC . “/cache.php”;
return;
}
if (!defined(“AOC_VERSION”)) define(“AOC_VERSION”, “5.8.1-memcached”);
if (!defined(“AOC_CACHE_TIER”)) define(“AOC_CACHE_TIER”, “memcached_only”);
if (!defined(“AOC_NS_PER_BLOG”)) define(“AOC_NS_PER_BLOG”, true);
if (!defined(“AOC_LOCAL_CACHE_LIMIT”)) define(“AOC_LOCAL_CACHE_LIMIT”, 70000);
$_legacy_fail_open = defined(“AOC_FAIL_OPEN”) ? AOC_FAIL_OPEN : null;
if (!defined(“AOC_FAIL_OPEN_WRITE”)) define(“AOC_FAIL_OPEN_WRITE”, $_legacy_fail_open ?? false);
unset($_legacy_fail_open);
if (!defined(“AOC_FAIL_OPEN_ADD”)) define(“AOC_FAIL_OPEN_ADD”, false);
if (!defined(“AOC_FAILOVER_TTL”)) define(“AOC_FAILOVER_TTL”, 15);
if (!defined(“AOC_DEFAULT_NOEXP_TTL”)) define(“AOC_DEFAULT_NOEXP_TTL”, 2592000);
if (!defined(“AOC_MC_COMPRESSION”)) define(“AOC_MC_COMPRESSION”, false);
if (!defined(“AOC_WOO_SKIP”)) define(“AOC_WOO_SKIP”, true);
if (!defined(“AOC_WOO_MODE”)) define(“AOC_WOO_MODE”, “strict”);
if (!defined(“AOC_KEY_TTL_MAX”)) define(“AOC_KEY_TTL_MAX”, 2591999);
if (!defined(“AOC_MAX_KEY_LENGTH”)) define(“AOC_MAX_KEY_LENGTH”, 230);
if (!defined(“AOC_BATCH_CHUNK”)) define(“AOC_BATCH_CHUNK”, 500);
if (!defined(“AOC_MGET_CHUNK”)) define(“AOC_MGET_CHUNK”, AOC_BATCH_CHUNK);
if (!defined(“AOC_PIPELINE_CHUNK”)) define(“AOC_PIPELINE_CHUNK”, AOC_BATCH_CHUNK);
if (!defined(“AOC_UNLINK_CHUNK”)) define(“AOC_UNLINK_CHUNK”, AOC_BATCH_CHUNK);
if (!defined(“AOC_STRICT_USER_PRIVACY”)) define(“AOC_STRICT_USER_PRIVACY”, false);
if (defined(“DONOTCACHEOBJECT”) && DONOTCACHEOBJECT) {
require_once ABSPATH . WPINC . “/cache.php”;
return;
}
function wp_cache_init() { $GLOBALS[“wp_object_cache”] = WP_Object_Cache::instance(); }
function wp_cache_add($key, $value, $group = “”, $expire = 0) { return WP_Object_Cache::instance()->add($key, $value, $group, (int) $expire); }
function wp_cache_set($key, $value, $group = “”, $expire = 0) { return WP_Object_Cache::instance()->set($key, $value, $group, (int) $expire); }
function wp_cache_replace($key, $value, $group = “”, $expire = 0) { return WP_Object_Cache::instance()->replace($key, $value, $group, (int) $expire); }
function wp_cache_get($key, $group = “”, $force = false, &$found = null) { return WP_Object_Cache::instance()->get($key, $group, (bool) $force, $found); }
function wp_cache_delete($key, $group = “”) { return WP_Object_Cache::instance()->delete($key, $group); }
function wp_cache_flush() { return WP_Object_Cache::instance()->flush(); }
function wp_cache_flush_runtime() { return WP_Object_Cache::instance()->flush_runtime(); }
function wp_cache_flush_group($group) { return WP_Object_Cache::instance()->flush_group($group); }
function wp_cache_incr($key, $offset = 1, $group = “”) { return WP_Object_Cache::instance()->incr($key, (int) $offset, $group); }
function wp_cache_decr($key, $offset = 1, $group = “”) { return WP_Object_Cache::instance()->decr($key, (int) $offset, $group); }
function wp_cache_close() { return WP_Object_Cache::instance()->close(); }
function wp_cache_get_multiple($keys, $group = “”, $force = false) { return WP_Object_Cache::instance()->get_multiple($keys, $group, (bool) $force); }
function wp_cache_set_multiple(array $data, $group = “”, $expire = 0): array { return WP_Object_Cache::instance()->set_multiple($data, $group, (int) $expire); }
function wp_cache_add_multiple(array $data, $group = “”, $expire = 0): array { return WP_Object_Cache::instance()->add_multiple($data, $group, (int) $expire); }
function wp_cache_delete_multiple(array $keys, $group = “”): array { return WP_Object_Cache::instance()->delete_multiple($keys, $group); }
function wp_cache_add_global_groups($groups) { WP_Object_Cache::instance()->add_global_groups($groups); }
function wp_cache_add_non_persistent_groups($groups) { WP_Object_Cache::instance()->add_non_persistent_groups($groups); }
function wp_cache_switch_to_blog($blog_id) { WP_Object_Cache::instance()->switch_to_blog($blog_id); }
function wp_cache_supports(string $feature): bool {
return match ($feature) {
“add_multiple”, “set_multiple”, “get_multiple”, “delete_multiple”, “flush_runtime”, “flush_group” => true,
default => false,
};
}
final class WP_Object_Cache
{
public int $cache_hits = 0;
public int $cache_misses = 0;
public int $cache_sets = 0;
private readonly bool $multisite;
private string $blog_prefix = “”;
private string $blog_prefix_colon = “”;
private readonly bool $woo_strict;
private readonly string $hash_algo;
private readonly int $cfg_failover_ttl;
private readonly bool $cfg_fail_open_write;
private readonly int $cfg_default_noexp_ttl;
private readonly int $mget_chunk_size;
private readonly int $pipeline_chunk_size;
private readonly int $unlink_chunk_size;
private readonly string $salt_np_prefix;
private readonly string $salt_gv_prefix;
private array $local = [];
private int $local_count = 0;
private array $np_groups = [“aoc_np” => true];
private array $global_groups = [];
/** @var \Memcached|null */
private $m = null;
private bool $m_ok = false;
private int $ns_cached = 0;
private bool $ns_initialized = false;
private array $gv_runtime = [];
private int $gv_runtime_count = 0;
private array $sanitized_groups = [];
private string $key_prefix = “”;
private bool $key_prefix_dirty = true;
/** @var array<string, true> */
private array $pending_ns_bumps = [];
/** @var array<string, array{0:string,1:string}> */
private array $pending_group_bumps = [];
private bool $bump_failure_logged = false;
private static ?WP_Object_Cache $_inst = null;
private static int $last_retry = 0;
private const FALSE_TOKEN = “\0__AOC_FALSE__\0″;
private const INVALID_KEY_CHARS = ” \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x7F”;
private const MC_RES_SUCCESS = 0;
private const MC_RES_NOTFOUND = 16;
private const MC_RES_NOTSTORED = 14;
public static function instance(): WP_Object_Cache
{
return self::$_inst ??= new self();
}
private function __construct()
{
$this->multisite = \function_exists(“is_multisite”) && \is_multisite();
$this->blog_prefix = $this->multisite ? (string) ($GLOBALS[“blog_id”] ?? 1) : “”;
$this->blog_prefix_colon = $this->blog_prefix !== “” ? $this->blog_prefix . “:” : “”;
$this->hash_algo = \in_array(“xxh3”, \hash_algos(), true) ? “xxh3” : “sha1”;
if (!defined(“AOC_KEY_SALT”)) {
if (defined(“WP_CACHE_KEY_SALT”) && WP_CACHE_KEY_SALT !== “”) {
define(“AOC_KEY_SALT”, WP_CACHE_KEY_SALT);
} else {
$seed = defined(“AUTH_KEY”) ? AUTH_KEY : (defined(“DB_NAME”) ? DB_NAME : (defined(“WP_HOME”) ? WP_HOME : ABSPATH));
define(“AOC_KEY_SALT”, \hash(“md5”, (string) $seed));
}
}
$this->salt_np_prefix = AOC_KEY_SALT . “:NP:”;
$this->salt_gv_prefix = AOC_KEY_SALT . “:GV:”;
$this->cfg_failover_ttl = (int) AOC_FAILOVER_TTL;
$this->cfg_fail_open_write = (bool) AOC_FAIL_OPEN_WRITE;
$this->cfg_default_noexp_ttl = (int) AOC_DEFAULT_NOEXP_TTL;
$this->mget_chunk_size = \max(1, (int) AOC_MGET_CHUNK);
$this->pipeline_chunk_size = \max(1, (int) AOC_PIPELINE_CHUNK);
$this->unlink_chunk_size = \max(1, (int) AOC_UNLINK_CHUNK);
$this->woo_strict = AOC_WOO_SKIP && defined(“AOC_WOO_MODE”) && AOC_WOO_MODE === “strict”;
if ($this->woo_strict) {
$this->np_groups[“term-queries”] = true;
}
if (\defined(“AOC_FORCE_NP_GROUPS”) && \is_array(\AOC_FORCE_NP_GROUPS)) {
foreach (\AOC_FORCE_NP_GROUPS as $g) {
$this->np_groups[$this->normalize_group($g)] = true;
}
}
if (defined(“AOC_STRICT_USER_PRIVACY”) && AOC_STRICT_USER_PRIVACY) {
foreach ([“users”, “userlogins”, “useremail”, “usermeta”, “user_meta”, “session_tokens”] as $g) {
$this->np_groups[$this->normalize_group($g)] = true;
}
}
$this->init_memcached();
$this->add_global_groups([
“users”, “userlogins”, “useremail”, “usermeta”, “user_meta”,
“site-options”, “site-lookup”,
“blog-details”, “blog-id-cache”, “blog-lookup”,
“networks”, “network-queries”,
“sites”, “site-details”, “site-queries”,
“global-posts”, “counts”, “plugins”,
]);
}
private function __clone(): void
{
throw new \LogicException(“Cannot clone singleton”);
}
public function __wakeup(): void
{
throw new \LogicException(“Cannot unserialize singleton”);
}
private function log_debug(string $msg): void
{
if (defined(“WP_DEBUG”) && WP_DEBUG && defined(“WP_DEBUG_LOG”) && WP_DEBUG_LOG) {
\error_log(“[AOC Memcached] ” . $msg);
}
}
private function mc_result_code(): int
{
if (!$this->m_ok || !$this->m) {
return self::MC_RES_NOTFOUND;
}
try {
return $this->m->getResultCode();
} catch (\Throwable $ex) {
return self::MC_RES_NOTFOUND;
}
}
private function init_memcached(): void
{
$this->m_ok = false;
if (!\class_exists(“Memcached”)) {
return;
}
try {
$persistent_id = “aoc_mc_” . \substr(\hash(“sha256”, AOC_KEY_SALT), 0, 16);
$this->m = new \Memcached($persistent_id);
if (!\count($this->m->getServerList())) {
$servers = defined(“WP_MEMCACHED_SERVERS”) ? WP_MEMCACHED_SERVERS : [“127.0.0.1:11211”];
if (\is_string($servers)) {
$servers = \explode(“,”, $servers);
}
foreach ($servers as $s) {
$s = \trim((string) $s);
if ($s === “”) {
continue;
}
$parts = \explode(“:”, $s, 2);
$host = $parts[0];
$port = isset($parts[1]) ? (int) $parts[1] : 11211;
$this->m->addServer($host, $port);
}
}
$this->m->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
$this->m->setOption(\Memcached::OPT_SERIALIZER, \Memcached::SERIALIZER_PHP);
$this->m->setOption(\Memcached::OPT_TCP_NODELAY, true);
$this->m->setOption(\Memcached::OPT_NO_BLOCK, true);
$this->m->setOption(\Memcached::OPT_CONNECT_TIMEOUT, 1500);
$this->m->setOption(\Memcached::OPT_RECV_TIMEOUT, 3000);
$this->m->setOption(\Memcached::OPT_SEND_TIMEOUT, 3000);
$this->m->setOption(\Memcached::OPT_SERVER_FAILURE_LIMIT, 3);
$this->m->setOption(\Memcached::OPT_REMOVE_FAILED_SERVERS, true);
$this->m->setOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE, true);
if (defined(“Memcached::OPT_DISTRIBUTION”)) {
$this->m->setOption(\Memcached::OPT_DISTRIBUTION, \Memcached::DISTRIBUTION_CONSISTENT);
}
if (defined(“Memcached::OPT_COMPRESSION”)) {
$this->m->setOption(\Memcached::OPT_COMPRESSION, (bool) AOC_MC_COMPRESSION);
}
if (defined(“WP_MEMCACHED_USERNAME”) && defined(“WP_MEMCACHED_PASSWORD”)) {
$this->m->setSaslAuthData(WP_MEMCACHED_USERNAME, WP_MEMCACHED_PASSWORD);
}
$this->m_ok = true;
} catch (\Throwable $ex) {
$this->log_debug(“Memcached init failed: ” . $ex->getMessage());
$this->m_ok = false;
}
}
private function check_memcached_recovery(): void
{
if ($this->m_ok) {
return;
}
$now = \time();
if ($now – self::$last_retry > $this->cfg_failover_ttl) {
self::$last_retry = $now;
$this->init_memcached();
if ($this->m_ok) {
$this->ns_initialized = false;
$this->gv_runtime = [];
$this->gv_runtime_count = 0;
$this->key_prefix_dirty = true;
// ✅ [FIX] Clear L1 cache on recovery to drop orphaned keys
$this->local = [];
$this->local_count = 0;
$this->bump_failure_logged = false;
$this->replay_pending_bumps_after_recovery();
}
}
}
private function normalize_group($group): string
{
$g = (string) $group;
return $g === “” ? “default” : $g;
}
private function normalize_keys($keys): array
{
$out = [];
foreach ((array) $keys as $k) {
if (\is_string($k) || \is_int($k)) {
$out[] = $k;
}
}
return $out;
}
private function local_exists(string $full): bool
{
return isset($this->local[$full]) || \array_key_exists($full, $this->local);
}
private function queue_pending_ns_bump(?string $ns_key = null): void
{
$this->pending_ns_bumps[$ns_key ?? $this->ns_key()] = true;
}
private function queue_pending_group_bump(string $pre, string $grp): void
{
$id = $pre . “\0” . $grp;
$this->pending_group_bumps[$id] = [$pre, $grp];
}
private function replay_pending_bumps_after_recovery(): void
{
if (!$this->m_ok) {
return;
}
if ($this->pending_ns_bumps) {
foreach (\array_keys($this->pending_ns_bumps) as $ns_key) {
unset($this->pending_ns_bumps[$ns_key]);
if (!$this->bump_ns_key($ns_key)) {
$this->pending_ns_bumps[$ns_key] = true;
return;
}
}
}
if ($this->pending_group_bumps) {
foreach ($this->pending_group_bumps as $id => [$pre, $grp]) {
unset($this->pending_group_bumps[$id]);
if (!$this->bump_group($pre, $grp)) {
$this->queue_pending_group_bump($pre, $grp);
break;
}
}
}
}
private function evict_gv_runtime(): void
{
if ($this->gv_runtime_count <= 2000) {
return;
}
foreach ($this->gv_runtime as $ek => $_) {
unset($this->gv_runtime[$ek]);
if (–$this->gv_runtime_count <= 1500) {
break;
}
}
}
private function flush_np_group_runtime(string $group): bool
{
$group_key = $this->sanitize_group_name($group);
$prefix_normal = $this->salt_np_prefix . $group_key . “:”;
$prefix_hashed = $this->salt_np_prefix . “H:” . $group_key . “:”;
$len_normal = \strlen($prefix_normal);
$len_hashed = \strlen($prefix_hashed);
foreach ($this->local as $full => $_) {
if (\strncmp($full, $prefix_normal, $len_normal) === 0 ||
\strncmp($full, $prefix_hashed, $len_hashed) === 0) {
unset($this->local[$full]);
if ($this->local_count > 0) {
–$this->local_count;
}
}
}
return true;
}
private function set_local(string $full, $val): void
{
// [v5.8.1] Prevent resources from entering L1 (cannot be serialized)
if (\is_resource($val)) {
return;
}
if (AOC_LOCAL_CACHE_LIMIT <= 0) {
return;
}
$exists = $this->local_exists($full);
if (!$exists && $this->local_count >= AOC_LOCAL_CACHE_LIMIT) {
$evict = \min(5000, \max(1000, (int) (AOC_LOCAL_CACHE_LIMIT * 0.02)));
while ($evict– > 0 && $this->local_count > 0) {
$old = \array_key_first($this->local);
if ($old === null) {
$this->local_count = 0;
break;
}
unset($this->local[$old]);
–$this->local_count;
}
}
if (!$exists) {
++$this->local_count;
}
$this->local[$full] = $val;
}
private function get_ttl_l2(int $exp): int
{
$ttl = $exp <= 0 ? $this->cfg_default_noexp_ttl : $exp;
return \min(\max(1, $ttl), AOC_KEY_TTL_MAX);
}
private function safe_hash(string $s): string
{
return \hash($this->hash_algo, $s);
}
private function sanitize_key(string $s): string
{
$len = \strlen($s);
return \strcspn($s, self::INVALID_KEY_CHARS) !== $len ? “h” . \hash($this->hash_algo, $s) : $s;
}
private function sanitize_group_name(string $group): string
{
if (isset($this->sanitized_groups[$group])) {
return $this->sanitized_groups[$group];
}
$len = \strlen($group);
return $this->sanitized_groups[$group] = ($len <= 64 && \strcspn($group, self::INVALID_KEY_CHARS) === $len) ? $group : “g” . $this->safe_hash($group);
}
private function update_key_prefix(): void
{
if ($this->key_prefix_dirty) {
$ns = $this->ns_initialized ? $this->ns_cached : $this->ns_ver();
$this->key_prefix = AOC_KEY_SALT . “:” . $ns . “:”;
$this->key_prefix_dirty = false;
}
}
private function is_valid_alloptions(string $group, $key, $value): bool
{
return !($group === “options” && (string) $key === “alloptions” && !\is_array($value));
}
private function is_wc_strict_key(string $key_s): bool
{
return \str_ends_with($key_s, “_cache_prefix”) ||
\str_starts_with($key_s, “wc_session_”) ||
\str_starts_with($key_s, “customer_session_”) ||
\str_contains($key_s, “wc_cart_hash”);
}
private function build_key($key, string $group, ?string &$full, bool &$is_np): bool
{
// [v5.8.1] Initialize reference to prevent uninitialized warning in strict mode
$full = null;
$key_s = \is_string($key) ? $key : (\is_int($key) ? (string) $key : “”);
if ($key_s === “”) {
return false;
}
$group_key = $this->sanitize_group_name($group);
// [v5.8.1] Removed redundant sanitized group check
$is_np = isset($this->np_groups[$group]);
if (!$is_np && $this->woo_strict && $this->is_wc_strict_key($key_s)) {
$is_np = true;
}
if ($is_np) {
$k = $this->sanitize_key($key_s);
$full = “{$this->salt_np_prefix}{$group_key}:{$k}”;
if (\strlen($full) > AOC_MAX_KEY_LENGTH) {
$full = “{$this->salt_np_prefix}H:{$group_key}:” . $this->safe_hash($key_s);
}
return true;
}
if (!$this->m_ok) {
$this->check_memcached_recovery();
}
$is_global = isset($this->global_groups[$group]);
$pre = !$is_global ? $this->blog_prefix_colon : “”;
$gv = $this->group_ver($pre, $group_key);
$k = $this->sanitize_key($key_s);
$this->update_key_prefix();
$full = $this->key_prefix . “{$pre}{$group_key}:v{$gv}:{$k}”;
if (\strlen($full) > AOC_MAX_KEY_LENGTH) {
$full = AOC_KEY_SALT . “:H:{$this->ns_cached}:{$gv}:” . $this->safe_hash(“{$pre}\0{$group_key}\0{$k}”);
}
return true;
}
private function ns_key(): string
{
return AOC_KEY_SALT . “:NS” . (AOC_NS_PER_BLOG && $this->multisite ? “:B” . $this->blog_prefix : “”);
}
private function gv_key(string $pre, string $grp): string
{
$key = “{$this->salt_gv_prefix}{$pre}:{$grp}”;
return \strlen($key) > AOC_MAX_KEY_LENGTH ? AOC_KEY_SALT . “:GVH:” . $this->safe_hash($pre . “\0” . $grp) : $key;
}
private function encode_value($val)
{
return false === $val ? self::FALSE_TOKEN : $val;
}
private function decode_value($raw)
{
return $raw === self::FALSE_TOKEN ? false : $raw;
}
private function normalize_counter_value($v): int|false
{
if ($v === false || $v === null) {
return false;
}
if (\is_int($v)) {
return $v > 0 ? $v : false;
}
if (\is_string($v) && \ctype_digit($v)) {
$n = (int) $v;
return $n > 0 ? $n : false;
}
return false;
}
private function ns_ver(): int
{
if ($this->ns_initialized) {
return $this->ns_cached;
}
$k = $this->ns_key();
try {
if (!$this->m_ok) {
$n = \time();
} else {
$v = $this->m->get($k);
$rc = $this->mc_result_code();
$n = $this->normalize_counter_value($v);
if ($n === false) {
if ($v === false && $rc === self::MC_RES_NOTFOUND) {
$n = \time();
$set = $this->m->add($k, $n);
if (!$set && $this->mc_result_code() === self::MC_RES_NOTSTORED) {
$v2 = $this->m->get($k);
$n2 = $this->normalize_counter_value($v2);
if ($n2 !== false) {
$n = $n2;
}
} elseif (!$set) {
$this->m_ok = false;
$n = 1;
}
} elseif ($rc === self::MC_RES_SUCCESS) {
$n = \time();
$this->m->set($k, $n);
} else {
$this->m_ok = false;
$n = 1;
}
}
}
$this->ns_cached = $n;
$this->ns_initialized = true;
$this->key_prefix_dirty = true;
return $this->ns_cached;
} catch (\Throwable $ex) {
$this->m_ok = false;
$this->ns_cached = 1;
$this->ns_initialized = true;
$this->key_prefix_dirty = true;
return 1;
}
}
private function group_ver(string $pre, string $grp): int
{
$k = $this->gv_key($pre, $grp);
if (isset($this->gv_runtime[$k])) {
return $this->gv_runtime[$k];
}
$this->evict_gv_runtime();
try {
if (!$this->m_ok) {
$n = \time();
} else {
$v = $this->m->get($k);
$rc = $this->mc_result_code();
$n = $this->normalize_counter_value($v);
if ($n === false) {
if ($v === false && $rc === self::MC_RES_NOTFOUND) {
$n = \time();
$set = $this->m->add($k, $n);
if (!$set && $this->mc_result_code() === self::MC_RES_NOTSTORED) {
$v2 = $this->m->get($k);
$n2 = $this->normalize_counter_value($v2);
if ($n2 !== false) {
$n = $n2;
}
} elseif (!$set) {
$this->m_ok = false;
$n = 1;
}
} elseif ($rc === self::MC_RES_SUCCESS) {
$n = \time();
$this->m->set($k, $n);
} else {
$this->m_ok = false;
$n = 1;
}
}
}
if (!isset($this->gv_runtime[$k])) {
++$this->gv_runtime_count;
}
$this->gv_runtime[$k] = $n;
return $n;
} catch (\Throwable $ex) {
$this->m_ok = false;
if (!isset($this->gv_runtime[$k])) {
++$this->gv_runtime_count;
}
$this->gv_runtime[$k] = 1;
return 1;
}
}
// RACE CONDITION WARNING: Memcached lacks atomic operations.
// This GET+INCREMENT sequence has a race window where concurrent requests
// may both see the same value and increment to the same result.
private function ensure_counter_seeded_for_bump(string $k): void
{
$v = $this->m->get($k);
$n = $this->normalize_counter_value($v);
if ($n !== false) {
return;
}
$seed = \time();
if ($v === false && $this->mc_result_code() === self::MC_RES_NOTFOUND) {
$this->m->add($k, $seed);
return;
}
$this->m->set($k, $seed);
}
private function bump_ns_key(string $k): bool
{
if (!$this->m_ok) {
$this->check_memcached_recovery();
if (!$this->m_ok) {
return false;
}
}
try {
$this->ensure_counter_seeded_for_bump($k);
$new = $this->m->increment($k, 1);
if ($new === false) {
$new = \max(\time(), $this->ns_cached + 1);
if (!$this->m->set($k, $new)) {
$this->m_ok = false;
return false;
}
}
$n = (int) $new;
if ($k === $this->ns_key()) {
$this->ns_cached = $n;
$this->ns_initialized = true;
$this->key_prefix_dirty = true;
}
return true;
} catch (\Throwable $ex) {
$this->m_ok = false;
return false;
}
}
private function bump_ns(): bool
{
return $this->bump_ns_key($this->ns_key());
}
private function bump_group_runtime_only(string $pre, string $grp): void
{
$key = $this->gv_key($pre, $grp);
$this->evict_gv_runtime();
if (!isset($this->gv_runtime[$key])) {
++$this->gv_runtime_count;
$this->gv_runtime[$key] = \time();
}
$this->gv_runtime[$key] = \max($this->gv_runtime[$key] + 1, \time() + 1);
}
private function bump_group(string $pre, string $grp): bool
{
if (!$this->m_ok) {
$this->check_memcached_recovery();
if (!$this->m_ok) {
$this->bump_group_runtime_only($pre, $grp);
return false;
}
}
$k = $this->gv_key($pre, $grp);
try {
$this->ensure_counter_seeded_for_bump($k);
$new = $this->m->increment($k, 1);
if ($new === false) {
$old = $this->gv_runtime[$k] ?? 0;
$new = \max(\time(), $old + 1);
if (!$this->m->set($k, $new)) {
$this->m_ok = false;
$this->bump_group_runtime_only($pre, $grp);
return false;
}
}
$this->evict_gv_runtime();
if (!isset($this->gv_runtime[$k])) {
++$this->gv_runtime_count;
}
$this->gv_runtime[$k] = (int) $new;
return true;
} catch (\Throwable $ex) {
$this->m_ok = false;
$this->bump_group_runtime_only($pre, $grp);
return false;
}
}
private function can_fail_open_write(string $full, ?string $mode): bool
{
if (!$this->cfg_fail_open_write) {
return false;
}
if (“add” === $mode && !AOC_FAIL_OPEN_ADD) {
return false;
}
if (“add” === $mode && $this->local_exists($full)) {
return false;
}
if (“replace” === $mode && !$this->local_exists($full)) {
return false;
}
return true;
}
private function set_l2(string $full, $val, int $exp, ?string $mode): bool
{
if (!$this->m_ok) {
if ($this->can_fail_open_write($full, $mode)) {
$this->set_local($full, $val);
return true;
}
return false;
}
try {
$enc = $this->encode_value($val);
$ttl = $this->get_ttl_l2($exp);
$res = $mode === “add” ? $this->m->add($full, $enc, $ttl) : ($mode === “replace” ? $this->m->replace($full, $enc, $ttl) : $this->m->set($full, $enc, $ttl));
if ($res) {
$this->set_local($full, $val);
return true;
}
$rc = $this->mc_result_code();
if ($mode === “add” && $rc === self::MC_RES_NOTSTORED) {
return false;
}
if ($mode === “replace” && ($rc === self::MC_RES_NOTSTORED || $rc === self::MC_RES_NOTFOUND)) {
return false;
}
$this->m_ok = false;
if ($this->can_fail_open_write($full, $mode)) {
$this->set_local($full, $val);
return true;
}
return false;
} catch (\Throwable $ex) {
$this->m_ok = false;
if ($this->can_fail_open_write($full, $mode)) {
$this->set_local($full, $val);
return true;
}
return false;
}
}
private function delete_l2(string $full): bool
{
if (!$this->m_ok) {
return false;
}
try {
$res = $this->m->delete($full);
if ($res) {
return true;
}
$rc = $this->mc_result_code();
if ($rc !== self::MC_RES_NOTFOUND && $rc !== self::MC_RES_SUCCESS) {
$this->m_ok = false;
}
return false;
} catch (\Throwable $ex) {
$this->m_ok = false;
return false;
}
}
public function add($key, $value, $group = “default”, int $expire = 0): bool
{
if (\function_exists(“wp_suspend_cache_addition”) && \wp_suspend_cache_addition()) {
return false;
}
return $this->set($key, $value, $group, $expire, “add”);
}
public function replace($key, $value, $group = “default”, int $expire = 0): bool
{
return $this->set($key, $value, $group, $expire, “replace”);
}
public function set($key, $value, $group = “default”, int $expire = 0, $mode = null): bool
{
$group = $this->normalize_group($group);
if (!$this->is_valid_alloptions($group, $key, $value)) {
return false;
}
$is_np = false;
if (!$this->build_key($key, $group, $full, $is_np)) {
return false;
}
if (“add” === $mode && $this->local_exists($full)) {
return false;
}
if ($is_np) {
if ($value instanceof \Closure) {
return false;
}
if (“replace” === $mode && !$this->local_exists($full)) {
return false;
}
$this->set_local($full, $value);
++$this->cache_sets;
return true;
}
$ok = $this->set_l2($full, $value, $expire, $mode);
if ($ok) {
++$this->cache_sets;
}
return $ok;
}
public function get($key, $group = “default”, $force = false, &$found = null)
{
$group = $this->normalize_group($group);
$force = (bool) $force;
$is_np = false;
if (!$this->build_key($key, $group, $full, $is_np)) {
$found = false;
return false;
}
if ($is_np || (!$force && $this->local_exists($full))) {
if ($this->local_exists($full)) {
$val = $this->local[$full];
if (!$this->is_valid_alloptions($group, $key, $val)) {
++$this->cache_misses;
$found = false;
return false;
}
++$this->cache_hits;
$found = true;
// [v5.8.1] Return reference directly — WordPress native semantics
return $val;
}
++$this->cache_misses;
$found = false;
return false;
}
if (!$this->m_ok) {
++$this->cache_misses;
$found = false;
return false;
}
try {
$raw = $this->m->get($full);
$rc = $this->mc_result_code();
if ($raw === false && $rc === self::MC_RES_NOTFOUND) {
++$this->cache_misses;
$found = false;
return false;
}
if ($raw === false && $rc !== self::MC_RES_SUCCESS) {
$this->m_ok = false;
++$this->cache_misses;
$found = false;
return false;
}
$val = $this->decode_value($raw);
if (!$this->is_valid_alloptions($group, $key, $val)) {
++$this->cache_misses;
$found = false;
return false;
}
$this->set_local($full, $val);
++$this->cache_hits;
$found = true;
// [v5.8.1] Return reference directly — WordPress native semantics
return $val;
} catch (\Throwable $ex) {
$this->m_ok = false;
++$this->cache_misses;
$found = false;
return false;
}
}
public function delete($key, $group = “default”): bool
{
$group = $this->normalize_group($group);
$is_np = false;
if (!$this->build_key($key, $group, $full, $is_np)) {
return false;
}
$had_local = false;
if (isset($this->local[$full])) {
unset($this->local[$full]);
if ($this->local_count > 0) {
–$this->local_count;
}
$had_local = true;
} elseif (\array_key_exists($full, $this->local)) {
unset($this->local[$full]);
if ($this->local_count > 0) {
–$this->local_count;
}
$had_local = true;
}
return $is_np ? $had_local : $this->delete_l2($full);
}
public function incr($key, int $offset = 1, $group = “default”)
{
$group = $this->normalize_group($group);
$is_np = false;
if (!$this->build_key($key, $group, $full, $is_np)) {
return false;
}
if ($is_np) {
if (!$this->local_exists($full)) {
return false;
}
$cur = $this->local[$full];
if (!\is_numeric($cur)) {
return false;
}
$new = (int) $cur + $offset;
if ($new < 0) {
$new = 0;
}
$this->local[$full] = $new;
return $new;
}
if (!$this->m_ok) {
return false;
}
try {
$new = $offset >= 0 ? $this->m->increment($full, $offset) : $this->m->decrement($full, -$offset);
if ($new !== false) {
$this->set_local($full, (int) $new);
return (int) $new;
}
$raw = $this->m->get($full);
$rc = $this->mc_result_code();
if ($raw === false && $rc === self::MC_RES_NOTFOUND) {
return false;
}
if ($raw === false && $rc !== self::MC_RES_SUCCESS) {
$this->m_ok = false;
return false;
}
$cur = $this->decode_value($raw);
if (!\is_numeric($cur)) {
return false;
}
$new = (int) $cur + $offset;
if ($new < 0) {
$new = 0;
}
if ($this->m->set($full, $new, $this->get_ttl_l2(0))) {
$this->set_local($full, $new);
return $new;
}
} catch (\Throwable $e) {
$this->m_ok = false;
}
return false;
}
public function decr($key, int $offset = 1, $group = “default”)
{
return $this->incr($key, -$offset, $group);
}
public function get_multiple($keys, $group = “default”, $force = false): array
{
$group = $this->normalize_group($group);
$force = (bool) $force;
$keys = $this->normalize_keys($keys);
$out = [];
$miss = [];
foreach ($keys as $k) {
$is_np = false;
if (!$this->build_key($k, $group, $full, $is_np)) {
$out[$k] = false;
++$this->cache_misses;
continue;
}
if ($is_np || (!$force && $this->local_exists($full))) {
if ($this->local_exists($full)) {
$val = $this->local[$full];
if (!$this->is_valid_alloptions($group, $k, $val)) {
$out[$k] = false;
++$this->cache_misses;
continue;
}
// [v5.8.1] Return reference directly — WordPress native semantics
$out[$k] = $val;
++$this->cache_hits;
} else {
$out[$k] = false;
++$this->cache_misses;
}
continue;
}
$miss[$full] = $k;
$out[$k] = false;
}
if (!$miss) {
return $out;
}
if (!$this->m_ok) {
$this->cache_misses += \count($miss);
return $out;
}
$missFulls = \array_keys($miss);
$chunks = \count($missFulls) <= $this->mget_chunk_size ? [$missFulls] : \array_chunk($missFulls, $this->mget_chunk_size);
foreach ($chunks as $chunk) {
try {
$vals = $this->m->getMulti($chunk);
if ($vals === false) {
$this->m_ok = false;
break;
}
foreach ($chunk as $full) {
$orig = $miss[$full];
if (\array_key_exists($full, $vals)) {
$decoded = $this->decode_value($vals[$full]);
if (!$this->is_valid_alloptions($group, $orig, $decoded)) {
$out[$orig] = false;
++$this->cache_misses;
continue;
}
$this->set_local($full, $decoded);
// [v5.8.1] Return reference directly — WordPress native semantics
$out[$orig] = $decoded;
++$this->cache_hits;
} else {
$out[$orig] = false;
++$this->cache_misses;
}
}
} catch (\Throwable $ex) {
$this->m_ok = false;
break;
}
}
return $out;
}
public function set_multiple(array $data, $group = “default”, int $expire = 0): array
{
$group = $this->normalize_group($group);
$out = \array_fill_keys(\array_keys($data), false);
$ttl = $this->get_ttl_l2($expire);
$pipe_vals = [];
$pipe_map = [];
foreach ($data as $k => $v) {
if (!$this->is_valid_alloptions($group, $k, $v)) {
continue;
}
$is_np = false;
if (!$this->build_key($k, $group, $full, $is_np)) {
continue;
}
if ($is_np) {
if ($v instanceof \Closure) {
continue;
}
$this->set_local($full, $v);
$out[$k] = true;
++$this->cache_sets;
continue;
}
$pipe_vals[$full] = $this->encode_value($v);
$pipe_map[$full] = [$k, $v];
}
if (!$pipe_vals) {
return $out;
}
if (!$this->m_ok) {
if ($this->cfg_fail_open_write) {
foreach ($pipe_map as $full => [$k, $v]) {
$this->set_local($full, $v);
$out[$k] = true;
++$this->cache_sets;
}
}
return $out;
}
$pipeFulls = \array_keys($pipe_vals);
$chunks = \count($pipeFulls) <= $this->pipeline_chunk_size ? [$pipeFulls] : \array_chunk($pipeFulls, $this->pipeline_chunk_size);
foreach ($chunks as $chunkFulls) {
try {
$batch = [];
foreach ($chunkFulls as $full) {
$batch[$full] = $pipe_vals[$full];
}
$res = $this->m->setMulti($batch, $ttl);
if (!$res) {
$this->m_ok = false;
break;
}
foreach ($chunkFulls as $full) {
[$k, $v] = $pipe_map[$full];
$this->set_local($full, $v);
$out[$k] = true;
++$this->cache_sets;
}
} catch (\Throwable $ex) {
$this->m_ok = false;
break;
}
}
if (!$this->m_ok && $this->cfg_fail_open_write) {
foreach ($pipe_map as $full => [$k, $v]) {
if ($out[$k] !== true) {
$this->set_local($full, $v);
$out[$k] = true;
++$this->cache_sets;
}
}
}
return $out;
}
public function add_multiple(array $data, $group = “default”, int $expire = 0): array
{
if (\function_exists(“wp_suspend_cache_addition”) && \wp_suspend_cache_addition()) {
return \array_fill_keys(\array_keys($data), false);
}
$group = $this->normalize_group($group);
$out = [];
$ttl = $this->get_ttl_l2($expire);
$pipe_vals = [];
$pipe_map = [];
$blocked_failopen = [];
foreach ($data as $k => $v) {
if (!$this->is_valid_alloptions($group, $k, $v)) {
$out[$k] = false;
continue;
}
$is_np = false;
if (!$this->build_key($k, $group, $full, $is_np)) {
$out[$k] = false;
continue;
}
if ($this->local_exists($full)) {
$out[$k] = false;
continue;
}
if ($is_np) {
if ($v instanceof \Closure) {
$out[$k] = false;
continue;
}
$this->set_local($full, $v);
$out[$k] = true;
++$this->cache_sets;
continue;
}
$pipe_vals[$full] = $this->encode_value($v);
$pipe_map[$full] = [$k, $v];
$out[$k] = false;
}
if (!$pipe_vals) {
return $out;
}
if (!$this->m_ok) {
if ($this->cfg_fail_open_write && AOC_FAIL_OPEN_ADD) {
foreach ($pipe_map as $full => [$k, $v]) {
if ($out[$k] !== true && !$this->local_exists($full)) {
$this->set_local($full, $v);
$out[$k] = true;
++$this->cache_sets;
}
}
}
return $out;
}
$pipeFulls = \array_keys($pipe_vals);
$chunks = \count($pipeFulls) <= $this->pipeline_chunk_size ? [$pipeFulls] : \array_chunk($pipeFulls, $this->pipeline_chunk_size);
foreach ($chunks as $chunkFulls) {
try {
foreach ($chunkFulls as $full) {
$res = $this->m->add($full, $pipe_vals[$full], $ttl);
$rc = $this->mc_result_code();
[$k, $v] = $pipe_map[$full];
if ($res) {
$this->set_local($full, $v);
$out[$k] = true;
++$this->cache_sets;
continue;
}
if ($rc === self::MC_RES_NOTSTORED) {
$blocked_failopen[$full] = true;
continue;
}
$this->m_ok = false;
break 2;
}
} catch (\Throwable $ex) {
$this->m_ok = false;
break;
}
}
if (!$this->m_ok && $this->cfg_fail_open_write && AOC_FAIL_OPEN_ADD) {
foreach ($pipe_map as $full => [$k, $v]) {
if (isset($blocked_failopen[$full])) {
continue;
}
if ($out[$k] !== true && !$this->local_exists($full)) {
$this->set_local($full, $v);
$out[$k] = true;
++$this->cache_sets;
}
}
}
return $out;
}
public function delete_multiple(array $keys, $group = “default”): array
{
$group = $this->normalize_group($group);
$keys = $this->normalize_keys($keys);
$out = \array_fill_keys($keys, false);
$dels = [];
$map = [];
foreach ($keys as $k) {
$is_np = false;
if (!$this->build_key($k, $group, $full, $is_np)) {
continue;
}
$had_local = false;
if (isset($this->local[$full])) {
unset($this->local[$full]);
if ($this->local_count > 0) {
–$this->local_count;
}
$had_local = true;
} elseif (\array_key_exists($full, $this->local)) {
unset($this->local[$full]);
if ($this->local_count > 0) {
–$this->local_count;
}
$had_local = true;
}
if ($is_np) {
$out[$k] = $had_local;
continue;
}
$dels[] = $full;
$map[$full] = $k;
}
if (!$dels || !$this->m_ok) {
return $out;
}
$chunks = \count($dels) <= $this->unlink_chunk_size ? [$dels] : \array_chunk($dels, $this->unlink_chunk_size);
foreach ($chunks as $chunk) {
try {
$this->m->deleteMulti($chunk);
foreach ($chunk as $full) {
$out[$map[$full]] = true;
}
} catch (\Throwable $ex) {
$this->m_ok = false;
break;
}
}
return $out;
}
public function flush(): bool
{
$this->flush_runtime();
$ok = $this->bump_ns();
if (!$ok) {
$this->queue_pending_ns_bump();
if (!$this->bump_failure_logged) {
$this->log_debug(“flush() failed: Memcached unavailable; pending namespace bump queued for best-effort replay only if Memcached recovers within this request.”);
$this->bump_failure_logged = true;
}
}
return $ok;
}
public function flush_group($group): bool
{
$group = $this->normalize_group($group);
if (isset($this->np_groups[$group])) {
$this->flush_np_group_runtime($group);
return true;
}
$is_global = isset($this->global_groups[$group]);
$pre = !$is_global ? $this->blog_prefix_colon : “”;
$group_key = $this->sanitize_group_name($group);
$should_cleanup = ($group === “options”) || ($this->local_count > (AOC_LOCAL_CACHE_LIMIT * 0.5));
if ($should_cleanup) {
$this->update_key_prefix();
$prefix = $this->key_prefix . $pre . $group_key . “:”;
$prefix_len = \strlen($prefix);
foreach ($this->local as $full => $_) {
if (\strncmp($full, $prefix, $prefix_len) === 0) {
unset($this->local[$full]);
if ($this->local_count > 0) {
–$this->local_count;
}
}
}
}
$ok = $this->bump_group($pre, $group_key);
if (!$ok) {
$this->queue_pending_group_bump($pre, $group_key);
if (!$this->bump_failure_logged) {
$this->log_debug(“flush_group({$group}) failed: Memcached unavailable; pending group bump queued for best-effort replay only if Memcached recovers within this request.”);
$this->bump_failure_logged = true;
}
}
return $ok;
}
public function flush_runtime(): bool
{
$this->local = [];
$this->local_count = 0;
$this->gv_runtime = [];
$this->gv_runtime_count = 0;
$this->cache_hits = $this->cache_misses = $this->cache_sets = 0;
$this->ns_initialized = false;
$this->key_prefix_dirty = true;
return true;
}
public function switch_to_blog($id): void
{
$this->blog_prefix = $this->multisite ? (string) $id : “”;
$this->blog_prefix_colon = $this->blog_prefix !== “” ? $this->blog_prefix . “:” : “”;
$this->flush_runtime();
}
public function add_global_groups($groups): void
{
foreach ((array) $groups as $g) {
$this->global_groups[$this->normalize_group($g)] = true;
}
}
public function add_non_persistent_groups($groups): void
{
foreach ((array) $groups as $g) {
$this->np_groups[$this->normalize_group($g)] = true;
}
}
public function stats(): string
{
$total = $this->cache_hits + $this->cache_misses;
$ratio = $total > 0 ? \round(($this->cache_hits / $total) * 100, 2) : 0;
return \sprintf(
“<p>Cache Hits: %d<br>Cache Misses: %d<br>Hit Ratio: %s%%<br>Memcached: %s<br>Local Keys: %d<br>Group Versions Cached: %d</p>”,
$this->cache_hits,
$this->cache_misses,
$ratio,
$this->m_ok ? “Connected” : “Disconnected”,
$this->local_count,
$this->gv_runtime_count
);
}
public function close(): bool
{
return true;
}
}

