Berikut adalah evaluasi mendalam mengenai fitur-fitur dan kelebihan dari script object-cache.php varian Redis (v5.7.39):
1. Operasi Atomik via Lua Scripting (Keunggulan Utama)
Perbedaan paling fundamental dan krusial dari varian Memcached adalah penggunaan Lua Scripting untuk operasi yang membutuhkan atomikitas.
BUMP_COUNTER_LUA: Digunakan untuk menaikkan versi Namespace (NS) atau Group (GV). Lua memastikan operasiGETdanSET/INCRdieksekusi secara atomik tanpa race condition, bahkan di bawah beban konkurensi tinggi. Script ini juga mempertahankan TTL yang ada pada counter.DECR_CLAMP_LUA: Menangani operasidecr()dengan aman, memastikan nilai tidak pernah menjadi negatif (diclamp ke 0).MIGRATE_COUNTER_LUA: Mengatasi skenario ketika sebuah key yang awalnya berupa data terserialisasi (misal: string angka) tiba-tiba di-increment. Lua akan membaca format serialisasi lama, mengkonversinya ke integer murni, dan menyimpannya kembali dengan TTL yang sesuai.
2. Arsitektur L1/L2 Cache & Native Reference Semantics
- L1 (Runtime) & L2 (Redis): Sama seperti varian sebelumnya, menyimpan data sementara di memori PHP (L1) untuk mengurangi round-trip ke Redis (L2) dalam satu request. Dibekali dengan sistem eviction cerdas untuk mencegah memory leak.
- Native Reference Semantics: Menghapus fungsi
safe_clone(). Cache hit kini mengembalikan referensi objek asli sesuai perilaku bawaan WordPress, menghemat overhead CPU 5-20ms per hit.
3. Optimasi Serializer Manual (SERIALIZER_NONE)
Pada versi v5.7.28, migrasi dari SERIALIZER_PHP ke SERIALIZER_NONE dilakukan. Artinya, script ini melakukan serialize/unserialize data secara manual menggunakan serialize() PHP.
- Kontrol Penuh & Cepat: Ini mengeliminasi Double-GET penalty dan memberikan kontrol presisi atas tipe data.
- Optimasi
decode_value(): Mampu mendeteksi format integer atau digit dengan cepat tanpa harus melakukan unserialize penuh, mempercepat proses pembacaan data counter/angka. - Penanganan
false: MenggunakanFALSE_TOKENuntuk membedakan antara cache miss dan nilaifalseyang sengaja disimpan.
4. Non-Blocking Deletion (UNLINK)
Pada operasi delete() dan delete_multiple(), script menggunakan metode unlink() bawaan Redis, bukan del().
- Kelebihan:
unlinkmenghapus key dari keyspace Redis secara instan dan menunda pembersihan memori di background thread. Hal ini mencegah Redis terblokir (latensi tinggi) saat menghapus key dalam jumlah besar.
5. Pipelining & Batch Chunking Agresif
- Mendukung fungsi batch (
get_multiple,set_multiple, dll.) dengan memanfaatkanRedis::PIPELINE. - Chunk Size Besar: Nilai default
AOC_BATCH_CHUNKdiset ke 1000 (dua kali lipat dari Memcached yang 500). Hal ini dimungkinkan karena Redis Pipeline sangat efisien dalam mengirimkan multiple command dalam satu network packet, secara dramatis mempercepat pemuatan options atau post meta.
6. Fail-Open, Auto-Recovery & Pending Bumps
- Fail-Open Mechanism: Saat Redis down, penulisan cache bisa di-fallback ke L1 agar situs tetap berjalan (mengikuti konfigurasi
AOC_FAIL_OPEN_WRITE). - Pending Bumps Replay: Jika gagal melakukan
flush()atauflush_group()saat Redis down, permintaan bump dimasukkan ke antrian dan dieksekusi ulang saat Redis pulih. - Auto-Recovery: Terdapat
check_redis_recovery(). Jika koneksi pulih, cache L1 otomatis dibersihkan untuk mencegah orphaned keys (data usang), dan versi GV/NS di-reset.
7. Manajemen TTL & Kapabilitas Lanjutan
- Explicit TTL Capping (
AOC_CAP_EXPLICIT_TTL): Fitur opsional yang memaksa TTL dari plugin/tema agar tidak melebihi batasAOC_KEY_TTL_MAX. Berguna untuk mencegah cache terlalu lama tersangkut. - Fail-Fast Guard: Jika
WP_REDIS_HOSTdidefinisikan sebagai string kosong, koneksi langsung di-skip tanpa mencoba timeout yang membuang waktu. - TCP Keepalive: Mengaktifkan
OPT_TCP_KEEPALIVEuntuk menjaga koneksi persisten tetap hidup, menghindari broken pipe di lingkungan load balancer atau container.
8. Integrasi WooCommerce & Privasi
- Sama seperti varian Memcached, memiliki WooCommerce Strict Mode yang memaksa key dinamis WooCommerce (session, cart hash) menjadi non-persistent.
- User Privacy Mode: Secara opsional dapat memaksa data sensitif pengguna (
usermeta,session_tokens) untuk tidak disimpan secara persisten di Redis.
Kesimpulan
Jika varian Memcached adalah solusi “best-effort” yang andal namun memiliki celah race condition, varian Redis ini adalah solusi enterprise-grade sejati. Penggunaan Lua Scripting menghilangkan race condition sepenuhnya, sementara pipelining, manual serialization, dan UNLINK menjadikannya sangat optimal untuk lingkungan WordPress dengan trafik ekstrem (High Traffic / Multisite / WooCommerce). Kode ini mengeksploitasi fitur lanjutan Redis tanpa mengorbankan kompatibilitas WordPress.
<?php
declare(strict_types=1);
/**
* Redis Object Cache – Stripped (v5.7.39 Reference Semantics & Minor Optimizations)
*
* Core Engine: v5.7.39 (Removed safe_clone for WP reference semantics, Minor isset optimization).
* Resilience: Best-effort current-request pending bumps replay during Redis outages.
*
* @package AOC
* @author irawan.office + Senior Audit
* @version 5.7.39
*
* Plugin Name: Redis Object Cache – Stripped (redis_only)
* Description: Drop-in minimal Redis-only object cache. Ultra-clean L1/L2 coherence. Ready-to-Ship High-Traffic Optimized Edition.
* Version: 5.7.39
* Author: irawan.office (stripped + patched + final audit + JIT micro optimized)
* Requires PHP: 8.1
*
* Changelog:
* 5.7.39 – [CRITICAL] Removed safe_clone() — restored WordPress native reference semantics for cache hits.
* 5.7.39 – [PERF] Eliminated 5-20ms overhead per cache hit from unnecessary object cloning.
* 5.7.39 – [PERF] Removed redundant isset() check in build_key() for NP group detection.
* 5.7.38 – [CRITICAL] Restored decode_value() null-return for corrupt data.
* 5.7.38 – [PERF] Removed ReflectionObject from hot-paths.
* 5.7.38 – [PERF] Restored ctype_digit optimization in normalize_counter_value.
* 5.7.37 – [CRITICAL] Fixed L1 local_count underflow across delete/delete_multiple/flush_group.
* 5.7.37 – [HARDEN] Prevented invalid value types (Closure/resource) entering L1 cache.
* 5.7.37 – [FIX] Hardened build_key() with $full initialization and normalized NP detection.
* 5.7.35 – [HYGIENE] L1 cache cleared on Redis recovery.
* 5.7.35 – [HYGIENE] Rate-limit bump failure logs.
* 5.7.35 – [HARDEN] Fail-fast guard for empty WP_REDIS_HOST.
* 5.7.35 – [HYGIENE] BUMP_COUNTER_LUA preserves existing TTL.
* 5.7.35 – [HARDEN] BUMP_COUNTER_LUA strict integer parsing.
* 5.7.32 – [CRITICAL] Added atomic BUMP_COUNTER_LUA for NS/GV counters.
* 5.7.30 – [CRITICAL] Atomic DECR_CLAMP_LUA for negative incr().
* 5.7.28 – [ARCH] Migrated from SERIALIZER_PHP to SERIALIZER_NONE.
* 5.7.28 – [CRITICAL] Eliminated Double-GET penalty.
*/
defined(“ABSPATH”) || exit();
if (\PHP_VERSION_ID < 80100) {
if (defined(“WP_DEBUG”) && WP_DEBUG && defined(“WP_DEBUG_LOG”) && WP_DEBUG_LOG) {
\error_log(“[AOC Redis] 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.7.39”); }
if (!defined(“AOC_CACHE_TIER”)) { define(“AOC_CACHE_TIER”, “redis_only”); }
if (!defined(“AOC_NS_PER_BLOG”)) { define(“AOC_NS_PER_BLOG”, true); }
// [v5.7.35] Memory Note: 70,000 L1 entries may consume ~30-50MB RAM per PHP-FPM worker.
// For high-traffic WooCommerce/BuddyPress sites, ensure PHP memory_limit >= 256M.
// On shared hosting with memory_limit 128M, consider lowering this to 20000-30000.
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”, 259200); }
if (!defined(“AOC_REDIS_TIMEOUT”)) { define(“AOC_REDIS_TIMEOUT”, 0.5); }
if (!defined(“AOC_REDIS_READ_TIMEOUT”)) { define(“AOC_REDIS_READ_TIMEOUT”, 1.0); }
if (!defined(“AOC_REDIS_PERSISTENT”)) { define(“AOC_REDIS_PERSISTENT”, true); }
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”, 604800); }
if (!defined(“AOC_MAX_KEY_LENGTH”)) { define(“AOC_MAX_KEY_LENGTH”, 230); }
if (!defined(“AOC_BATCH_CHUNK”)) { define(“AOC_BATCH_CHUNK”, 1000); }
if (!defined(“AOC_MGET_CHUNK”)) { define(“AOC_MGET_CHUNK”, 1000); }
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_CAP_EXPLICIT_TTL”)) { define(“AOC_CAP_EXPLICIT_TTL”, false); }
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 readonly bool $cfg_cap_explicit_ttl;
private array $local = [];
private int $local_count = 0;
private array $np_groups = [“aoc_np” => true];
private array $global_groups = [];
private $redis = null;
private bool $redis_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 BUMP_COUNTER_LUA = <<<‘LUA’
local val = redis.call(‘GET’, KEYS[1])
local seed = tonumber(ARGV[1])
local n = nil
if val ~= false then
local raw = string.match(val, “^-?%d+$”)
if raw ~= nil then n = tonumber(raw) end
if n == nil then n = tonumber(string.match(val, “^i:(-?%d+);$”)) end
if n == nil then
local s = string.match(val, ‘^s:%d+:”(-?%d+)”;$’)
if s ~= nil then n = tonumber(s) end
end
end
if n == nil or n <= 1 then n = seed end
n = n + 1
local current_ttl = redis.call(‘TTL’, KEYS[1])
if current_ttl > 0 then
redis.call(‘SET’, KEYS[1], tostring(n), ‘EX’, current_ttl)
else
redis.call(‘SET’, KEYS[1], tostring(n))
end
return n
LUA;
private const MIGRATE_COUNTER_LUA = <<<‘LUA’
local val = redis.call(‘GET’, KEYS[1])
local off = tonumber(ARGV[1])
local default_ttl = tonumber(ARGV[2])
if val == false then
if off < 0 then return false end
local new = off
if new < 0 then new = 0 end
if default_ttl ~= nil and default_ttl > 0 then
redis.call(‘SET’, KEYS[1], tostring(new), ‘EX’, default_ttl)
else
redis.call(‘SET’, KEYS[1], tostring(new))
end
return new
end
local n = tonumber(val)
if n == nil then n = tonumber(string.match(val, “^i:(-?%d+);$”)) end
if n == nil then
local s = string.match(val, ‘^s:%d+:”(-?%d+)”;$’)
if s ~= nil then n = tonumber(s) end
end
if n == nil then return false end
local new = n + off
if new < 0 then new = 0 end
local current_ttl = redis.call(‘TTL’, KEYS[1])
local ttl_to_use = default_ttl
if current_ttl > 0 then ttl_to_use = current_ttl end
if ttl_to_use ~= nil and ttl_to_use > 0 then
redis.call(‘SET’, KEYS[1], tostring(new), ‘EX’, ttl_to_use)
else
redis.call(‘SET’, KEYS[1], tostring(new))
end
return new
LUA;
private const DECR_CLAMP_LUA = <<<‘LUA’
local val = redis.call(‘GET’, KEYS[1])
local off = tonumber(ARGV[1])
local default_ttl = tonumber(ARGV[2])
if val == false then return false end
local n = tonumber(val)
if n == nil then return redis.error_reply(‘not an integer’) end
local cur_ttl = redis.call(‘TTL’, KEYS[1])
local new = n + off
local clamped = 0
if new < 0 then
new = 0
clamped = 1
end
if cur_ttl > 0 then
redis.call(‘SET’, KEYS[1], tostring(new), ‘EX’, cur_ttl)
elseif clamped == 1 and default_ttl ~= nil and default_ttl > 0 then
redis.call(‘SET’, KEYS[1], tostring(new), ‘EX’, default_ttl)
else
redis.call(‘SET’, KEYS[1], tostring(new))
end
return new
LUA;
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->cfg_cap_explicit_ttl = \defined(“AOC_CAP_EXPLICIT_TTL”) && AOC_CAP_EXPLICIT_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_redis();
$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 Redis] ” . $msg);
}
}
private function init_redis(): void
{
$this->redis_ok = false;
if (!\class_exists(“Redis”)) {
return;
}
// [v5.7.35] Fail-fast if WP_REDIS_HOST is explicitly empty/blank.
if (\defined(“WP_REDIS_HOST”) && \trim((string) WP_REDIS_HOST) === “”) {
$this->log_debug(“Redis init skipped: WP_REDIS_HOST is explicitly empty.”);
return;
}
try {
if (null !== $this->redis) {
try { $this->redis->close(); } catch (\Throwable $e) {}
}
$this->redis = new \Redis();
$host = defined(“WP_REDIS_HOST”) ? (string) WP_REDIS_HOST : “127.0.0.1”;
$port = defined(“WP_REDIS_PORT”) ? (int) WP_REDIS_PORT : 6379;
if ($host !== “” && (\str_starts_with($host, “unix://”) || ($host[0] === “/” && !\str_contains($host, “:”)))) {
$host = \str_replace(“unix://”, “”, $host);
$port = 0;
}
$timeout = (float) AOC_REDIS_TIMEOUT;
$db = defined(“WP_REDIS_DATABASE”) ? WP_REDIS_DATABASE : 0;
$user = defined(“WP_REDIS_USERNAME”) ? (string) WP_REDIS_USERNAME : “”;
$pass_sig = defined(“WP_REDIS_PASSWORD”) ? \substr(\hash(“sha256”, (string) WP_REDIS_PASSWORD), 0, 12) : “”;
$conn_id = “aoc-” . \substr(\hash(“sha256”, \implode(“|”, [
AOC_KEY_SALT, $host, (string) $port, (string) $db, $user, $pass_sig, “serializer_none”,
])), 0, 24);
$connected = AOC_REDIS_PERSISTENT
? $this->redis->pconnect($host, $port, $timeout, $conn_id)
: $this->redis->connect($host, $port, $timeout);
if (!$connected) {
return;
}
$this->redis->setOption(\Redis::OPT_READ_TIMEOUT, (float) AOC_REDIS_READ_TIMEOUT);
try {
$this->redis->setOption(\Redis::OPT_TCP_KEEPALIVE, 60);
} catch (\Throwable $e) {}
$authOk = true;
if (defined(“WP_REDIS_USERNAME”) && “” !== WP_REDIS_USERNAME && defined(“WP_REDIS_PASSWORD”)) {
$authOk = $this->redis->auth([(string) WP_REDIS_USERNAME, (string) WP_REDIS_PASSWORD]);
} elseif (defined(“WP_REDIS_PASSWORD”) && “” !== WP_REDIS_PASSWORD) {
$authOk = $this->redis->auth(WP_REDIS_PASSWORD);
}
if ($authOk !== true) {
$this->log_debug(“FATAL: Redis auth failed. Cache disabled.”);
try { $this->redis->close(); } catch (\Throwable $e) {}
$this->redis = null;
$this->redis_ok = false;
return;
}
if (defined(“WP_REDIS_DATABASE”)) {
$selectOk = $this->redis->select((int) WP_REDIS_DATABASE);
if ($selectOk !== true) {
$this->log_debug(“FATAL: Redis select DB failed. Cache disabled.”);
try { $this->redis->close(); } catch (\Throwable $e) {}
$this->redis = null;
$this->redis_ok = false;
return;
}
}
$this->redis->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_NONE);
if ($this->redis->getOption(\Redis::OPT_SERIALIZER) !== \Redis::SERIALIZER_NONE) {
$this->log_debug(“FATAL: Gagal memaksa SERIALIZER_NONE. Redis cache disabled for safety.”);
try { $this->redis->close(); } catch (\Throwable $e) {}
$this->redis = null;
$this->redis_ok = false;
return;
}
$this->redis_ok = true;
} catch (\Throwable $ex) {
$this->log_debug(“Redis init failed: ” . $ex->getMessage());
}
}
private function discard_pipeline(): void
{
$this->redis_ok = false;
if ($this->redis) {
try { $this->redis->discard(); } catch (\Throwable $e) {}
}
}
private function check_redis_recovery(): void
{
if ($this->redis_ok) {
return;
}
$now = \time();
if ($now – self::$last_retry > $this->cfg_failover_ttl) {
self::$last_retry = $now;
$this->init_redis();
if ($this->redis_ok) {
$this->ns_initialized = false;
$this->gv_runtime = [];
$this->gv_runtime_count = 0;
$this->key_prefix_dirty = true;
$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->redis_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.7.37] Prevent resources from entering L1 (cannot be serialized/cloned)
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
{
if ($exp <= 0) {
return \min(\max(1, $this->cfg_default_noexp_ttl), AOC_KEY_TTL_MAX);
}
if ($this->cfg_cap_explicit_ttl) {
return \min(\max(1, $exp), AOC_KEY_TTL_MAX);
}
return \max(1, $exp);
}
private function safe_hash(string $s): string
{
return \hash($this->hash_algo, $s);
}
private function sanitize_key(string $s): string
{
$len = \strlen($s);
if (\strcspn($s, self::INVALID_KEY_CHARS) !== $len) {
return “h” . \hash($this->hash_algo, $s);
}
return $s;
}
private function sanitize_group_name(string $group): string
{
if (isset($this->sanitized_groups[$group])) {
return $this->sanitized_groups[$group];
}
$len = \strlen($group);
if ($len <= 64 && \strcspn($group, self::INVALID_KEY_CHARS) === $len) {
return $this->sanitized_groups[$group] = $group;
}
return $this->sanitized_groups[$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.7.37] 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.7.39] Removed redundant $group_key check — np_groups uses raw group names only
$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->redis_ok) {
$this->check_redis_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
{
return “{$this->salt_gv_prefix}{$pre}:{$grp}”;
}
private function encode_value($val): string
{
return match (true) {
false === $val => self::FALSE_TOKEN,
null === $val => “N;”,
default => \serialize($val),
};
}
/**
* [v5.7.38] Returns null for corrupt/unrecognizable data.
* Callers MUST check: if ($val === null && $raw !== “N;”) → treat as cache miss.
*/
private function decode_value(string $raw)
{
if ($raw === self::FALSE_TOKEN) {
return false;
}
if ($raw !== “” && $raw !== “N;”) {
if ($raw[0] === “-“) {
if (\strlen($raw) > 1 && \ctype_digit(\substr($raw, 1))) {
return (int) $raw;
}
} elseif (\ctype_digit($raw)) {
return (int) $raw;
}
}
$val = @\unserialize($raw, [“allowed_classes” => true]);
if ($val === false && $raw !== “b:0;”) {
return null;
}
return $val;
}
private function normalize_counter_value($v, string $k): int|false
{
if ($v === false || $v === null) {
return false;
}
if (\is_int($v)) {
return $v > 0 ? $v : false;
}
$s = (string) $v;
if ($s !== “” && \ctype_digit($s)) {
$n = (int) $s;
return $n > 0 ? $n : false;
}
if (\preg_match(‘/^i:(\d+);$/’, $s, $m)) {
$n = (int) $m[1];
return $n > 0 ? $n : false;
}
if (\preg_match(‘/^s:\d+:”(\d+)”;$/’, $s, $m)) {
$n = (int) $m[1];
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 {
$v = $this->redis_ok ? $this->redis->get($k) : false;
$n = $this->normalize_counter_value($v, $k);
if ($n === false) {
$n = \time();
if ($this->redis_ok) {
if ($v === false || $v === null) {
$set = $this->redis->set($k, (string) $n, [“NX”]);
if (!$set) {
$v2 = $this->redis->get($k);
$n2 = $this->normalize_counter_value($v2, $k);
if ($n2 !== false) {
$n = $n2;
}
}
} else {
$this->redis->set($k, (string) $n);
}
}
}
$this->ns_cached = $n;
$this->ns_initialized = true;
$this->key_prefix_dirty = true;
return $this->ns_cached;
} catch (\Throwable $ex) {
$this->redis_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 {
$v = $this->redis_ok ? $this->redis->get($k) : false;
$n = $this->normalize_counter_value($v, $k);
if ($n === false) {
$n = \time();
if ($this->redis_ok) {
if ($v === false || $v === null) {
$set = $this->redis->set($k, (string) $n, [“NX”]);
if (!$set) {
$v2 = $this->redis->get($k);
$n2 = $this->normalize_counter_value($v2, $k);
if ($n2 !== false) {
$n = $n2;
}
}
} else {
$this->redis->set($k, (string) $n);
}
}
}
if (!isset($this->gv_runtime[$k])) {
++$this->gv_runtime_count;
}
$this->gv_runtime[$k] = $n;
return $n;
} catch (\Throwable $ex) {
$this->redis_ok = false;
if (!isset($this->gv_runtime[$k])) {
++$this->gv_runtime_count;
}
$this->gv_runtime[$k] = 1;
return 1;
}
}
private function bump_counter_atomic(string $k): int|false
{
try {
$res = $this->redis->eval(
self::BUMP_COUNTER_LUA,
[$k, (string) \time()],
1
);
if ($res === false || $res === null || !\is_numeric($res)) {
return false;
}
$n = (int) $res;
return $n > 0 ? $n : false;
} catch (\Throwable $e) {
$this->redis_ok = false;
return false;
}
}
private function bump_ns_key(string $k): bool
{
if (!$this->redis_ok) {
$this->check_redis_recovery();
if (!$this->redis_ok) {
return false;
}
}
$n = $this->bump_counter_atomic($k);
if ($n === false) {
$this->redis_ok = false;
return false;
}
if ($k === $this->ns_key()) {
$this->ns_cached = $n;
$this->ns_initialized = true;
$this->key_prefix_dirty = true;
}
return true;
}
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->redis_ok) {
$this->check_redis_recovery();
if (!$this->redis_ok) {
$this->bump_group_runtime_only($pre, $grp);
return false;
}
}
$k = $this->gv_key($pre, $grp);
$n = $this->bump_counter_atomic($k);
if ($n === false) {
$this->redis_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] = $n;
return true;
}
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->redis_ok) {
if ($this->can_fail_open_write($full, $mode)) {
$this->set_local($full, $val);
return true;
}
return false;
}
try {
$enc = $this->encode_value($val);
$opts = [];
if (“add” === $mode) {
$opts[] = “NX”;
} elseif (“replace” === $mode) {
$opts[] = “XX”;
}
$ttl = $this->get_ttl_l2($exp);
if ($ttl > 0) {
$opts[“EX”] = $ttl;
}
$res = (bool) $this->redis->set($full, $enc, $opts);
if ($res) {
$this->set_local($full, $val);
}
return $res;
} catch (\Throwable $ex) {
$this->redis_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->redis_ok) {
return false;
}
try {
$res = $this->redis->unlink($full);
return \is_int($res) ? $res > 0 : $res === true;
} catch (\Throwable $ex) {
$this->redis_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)) {
if (!$is_np && $this->redis_ok && $this->cfg_fail_open_write && AOC_FAIL_OPEN_ADD) {
try {
$exists = $this->redis->exists($full);
if ($exists > 0 || $exists === true) {
return false;
}
unset($this->local[$full]);
if ($this->local_count > 0) {
–$this->local_count;
}
} catch (\Throwable $ex) {
$this->redis_ok = false;
return false;
}
} else {
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) {
if ($this->local_exists($full)) {
$val = $this->local[$full];
if ($group === “options” && $key === “alloptions” && !\is_array($val)) {
++$this->cache_misses;
$found = false;
return false;
}
++$this->cache_hits;
$found = true;
// [v5.7.39] Return reference directly — WordPress native semantics
return $val;
}
++$this->cache_misses;
$found = false;
return false;
}
if (!$force && $this->local_exists($full)) {
$val = $this->local[$full];
if ($group === “options” && $key === “alloptions” && !\is_array($val)) {
++$this->cache_misses;
$found = false;
return false;
}
++$this->cache_hits;
$found = true;
// [v5.7.39] Return reference directly — WordPress native semantics
return $val;
}
if (!$this->redis_ok) {
++$this->cache_misses;
$found = false;
return false;
}
try {
$raw = $this->redis->get($full);
if ($raw === false) {
++$this->cache_misses;
$found = false;
return false;
}
$val = $this->decode_value($raw);
if ($val === null && $raw !== “N;”) {
++$this->cache_misses;
$found = false;
return false;
}
if ($group === “options” && $key === “alloptions” && !\is_array($val)) {
++$this->cache_misses;
$found = false;
return false;
}
$this->set_local($full, $val);
++$this->cache_hits;
$found = true;
// [v5.7.39] Return reference directly — WordPress native semantics
return $val;
} catch (\Throwable $ex) {
$this->redis_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;
}
if ($is_np) {
return $had_local;
}
return $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 = \max(0, (int) $cur + $offset);
$this->local[$full] = $new;
return $new;
}
if (!$this->redis_ok) {
return false;
}
$ttl = $this->get_ttl_l2(0);
if ($offset < 0) {
try {
$res = $this->redis->eval(
self::DECR_CLAMP_LUA,
[$full, (string) $offset, (string) $ttl],
1
);
if ($res === false || $res === null || !\is_numeric($res)) {
return false;
}
$result = (int) $res;
$this->set_local($full, $result);
return $result;
} catch (\Throwable $e) {
if (\str_contains($e->getMessage(), “not an integer”) || \str_contains($e->getMessage(), “WRONGTYPE”)) {
return $this->migrate_serialized_counter_and_incr($full, $offset, $ttl);
}
$this->redis_ok = false;
return false;
}
}
try {
$result = $this->redis->rawCommand(“INCRBY”, $full, (string) $offset);
if ($result === false || $result === null) {
return false;
}
$result = (int) $result;
if ($result < 0) {
try {
$res = $this->redis->eval(
self::DECR_CLAMP_LUA,
[$full, “0”, (string) $ttl],
1
);
if ($res === false || $res === null || !\is_numeric($res)) {
return false;
}
$result = (int) $res;
} catch (\Throwable $e) {
$this->redis_ok = false;
return false;
}
} elseif ($result === $offset && $ttl > 0) {
$cur_ttl = $this->redis->ttl($full);
if ($cur_ttl === -1) {
$this->redis->expire($full, $ttl);
}
}
$this->set_local($full, $result);
return $result;
} catch (\Throwable $e) {
if (\str_contains($e->getMessage(), “not an integer”) || \str_contains($e->getMessage(), “WRONGTYPE”)) {
return $this->migrate_serialized_counter_and_incr($full, $offset, $ttl);
}
$this->redis_ok = false;
return false;
}
}
private function migrate_serialized_counter_and_incr(string $full, int $offset, int $default_ttl): int|false
{
try {
$res = $this->redis->eval(
self::MIGRATE_COUNTER_LUA,
[$full, (string) $offset, (string) $default_ttl],
1
);
if ($res === false || $res === null || !\is_numeric($res)) {
return false;
}
$new = \max(0, (int) $res);
$this->set_local($full, $new);
return $new;
} catch (\Throwable $e) {
$this->redis_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) {
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.7.39] Return reference directly — WordPress native semantics
$out[$k] = $val;
++$this->cache_hits;
} else {
$out[$k] = false;
++$this->cache_misses;
}
continue;
}
if (!$force && $this->local_exists($full)) {
$val = $this->local[$full];
if (!$this->is_valid_alloptions($group, $k, $val)) {
$out[$k] = false;
++$this->cache_misses;
continue;
}
// [v5.7.39] Return reference directly — WordPress native semantics
$out[$k] = $val;
++$this->cache_hits;
} else {
$miss[$full] = $k;
$out[$k] = false;
}
}
if (empty($miss)) {
return $out;
}
if (!$this->redis_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->redis->mget($chunk);
if (!\is_array($vals)) {
$this->redis_ok = false;
break;
}
foreach ($vals as $i => $v) {
$full = $chunk[$i];
$orig = $miss[$full];
if (false !== $v) {
$decoded = $this->decode_value($v);
if ($decoded === null && $v !== “N;”) {
$out[$orig] = false;
++$this->cache_misses;
continue;
}
if (!$this->is_valid_alloptions($group, $orig, $decoded)) {
$out[$orig] = false;
++$this->cache_misses;
continue;
}
$this->set_local($full, $decoded);
// [v5.7.39] Return reference directly — WordPress native semantics
$out[$orig] = $decoded;
++$this->cache_hits;
} else {
$out[$orig] = false;
++$this->cache_misses;
}
}
} catch (\Throwable $ex) {
$this->redis_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);
$opts = [];
if ($ttl > 0) {
$opts[“EX”] = $ttl;
}
$pipe_vals = [];
$pipe_map = [];
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 ($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] = $v;
$pipe_map[$full] = $k;
}
if (!$pipe_vals) {
return $out;
}
if (!$this->redis_ok) {
if ($this->cfg_fail_open_write) {
foreach ($pipe_vals as $full => $v) {
$this->set_local($full, $v);
$out[$pipe_map[$full]] = 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 $chunk_fulls) {
try {
$this->redis->multi(\Redis::PIPELINE);
foreach ($chunk_fulls as $full) {
$this->redis->set($full, $this->encode_value($pipe_vals[$full]), $opts);
}
$res = $this->redis->exec();
if (!\is_array($res)) {
$this->redis_ok = false;
break;
}
foreach ($chunk_fulls as $i => $full) {
if (isset($res[$i]) && false !== $res[$i]) {
$this->set_local($full, $pipe_vals[$full]);
$out[$pipe_map[$full]] = true;
++$this->cache_sets;
}
}
} catch (\Throwable $ex) {
$this->discard_pipeline();
break;
}
}
if (!$this->redis_ok && $this->cfg_fail_open_write) {
foreach ($pipe_vals as $full => $v) {
$orig = $pipe_map[$full];
if (true !== $out[$orig]) {
$this->set_local($full, $v);
$out[$orig] = 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);
$opts = [“NX”];
if ($ttl > 0) {
$opts[“EX”] = $ttl;
}
$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] = $v;
$pipe_map[$full] = $k;
$out[$k] = false;
}
if (!$pipe_vals) {
return $out;
}
if (!$this->redis_ok) {
if ($this->cfg_fail_open_write && AOC_FAIL_OPEN_ADD) {
foreach ($pipe_vals as $full => $v) {
$orig = $pipe_map[$full];
if (!$this->local_exists($full)) {
$this->set_local($full, $v);
$out[$orig] = 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 $chunk_fulls) {
try {
$this->redis->multi(\Redis::PIPELINE);
foreach ($chunk_fulls as $full) {
$this->redis->set($full, $this->encode_value($pipe_vals[$full]), $opts);
}
$res = $this->redis->exec();
if (!\is_array($res)) {
$this->redis_ok = false;
break;
}
foreach ($chunk_fulls as $i => $full) {
$ok = isset($res[$i]) && false !== $res[$i];
$orig = $pipe_map[$full];
$out[$orig] = $ok;
if ($ok) {
$this->set_local($full, $pipe_vals[$full]);
++$this->cache_sets;
} else {
$blocked_failopen[$full] = true;
}
}
} catch (\Throwable $ex) {
$this->discard_pipeline();
break;
}
}
if (!$this->redis_ok && $this->cfg_fail_open_write && AOC_FAIL_OPEN_ADD) {
foreach ($pipe_vals as $full => $v) {
if (isset($blocked_failopen[$full])) {
continue;
}
$orig = $pipe_map[$full];
if (!$this->local_exists($full)) {
$this->set_local($full, $v);
$out[$orig] = 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) {
return $out;
}
if (!$this->redis_ok) {
return $out;
}
$chunks = \count($dels) <= $this->unlink_chunk_size
? [$dels]
: \array_chunk($dels, $this->unlink_chunk_size);
foreach ($chunks as $chunk) {
try {
$result = $this->redis->unlink($chunk);
if ($result !== false) {
foreach ($chunk as $full) {
$out[$map[$full]] = true;
}
} else {
$this->redis_ok = false;
break;
}
} catch (\Throwable $ex) {
$this->redis_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: Redis unavailable; pending namespace bump queued for best-effort replay only if Redis 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: Redis unavailable; pending group bump queued for best-effort replay only if Redis 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>Redis: %s<br>Local Keys: %d<br>Group Versions Cached: %d</p>”,
$this->cache_hits,
$this->cache_misses,
$ratio,
$this->redis_ok ? “Connected” : “Disconnected”,
$this->local_count,
$this->gv_runtime_count
);
}
public function close(): bool
{
if ($this->redis) {
try {
if (!AOC_REDIS_PERSISTENT) {
if ($this->redis->isConnected()) {
$this->redis->close();
}
$this->redis = null;
$this->redis_ok = false;
}
} catch (\Throwable $e) {}
}
return true;
}
}



Test
tes