Hey Go,
On 16.5.2026 17:19:31, Go Kudo wrote:
Hi internals,
I'd like to start the discussion for a new RFC, OPcache Static Cache.
RFC: https://wiki.php.net/rfc/opcache_static_cache
Implementation: https://github.com/php/php-src/pull/22052
The proposal adds an OPcache-managed shared-memory cache for explicit
userland values and for selected PHP static state. It introduces
explicit functions under the OPcache namespace (volatile_* and
persistent_*) and two attributes, #[OPcache\VolatileStatic] and
#[OPcache\PersistentStatic], that let selected static properties and
method static variables survive across requests. The feature is
disabled by default and only activates once memory is allocated
through the new INI directives.
The RFC covers the motivation, the deliberate split between the two
backends, the trust model (one PHP runtime = one trust domain; this is
not a tenant isolation boundary), and benchmarks against APCu on NTS
php-fpm and ZTS FrankenPHP. The PR is the full implementation, with
PHPT coverage summarized in the Validation section.
Feedback welcome.
Best Regards,
Go Kudo
I've been trying to digest the RFC and it's quite long-winded.
The one thing I don't grasp is the "Security and Trust Model". Sure, if
you have the worker pools, like in fpm, it absolutely makes sense to use it.
But why is it fundamentally required to have this sort of separation?
What's the point? It just means that the whole webserver is a single
boundary rather than having the ability to split more precisely. Which
in the end is a configuration / system administration setup issue,
rather than a fundamental flaw.
To me it's a non-starter to exclude apache2handler SAPI from this feature.
Regarding the API:
I think it might make sense to make the caches non-static classes, with
a constructor accepting an optional arbitrary namespace; further split
the overloaded behaviour of delete() and getCacheStoreType() - let's not
mix classes and arbitrary keys:
abstract class Cache {
public abstract function get(string $key,
null|bool|int|float|string|array|object $default = null):
null|bool|int|float|string|array|object;
public abstract function getMultiple(array $keys, ?array $default =
null): array|false;
public abstract function set(string $key,
null|bool|int|float|string|array|object $value): bool;
public abstract function setMultiple(array $values): bool;
public abstract function has(string $key): bool;
public abstract function delete(string $key): bool;
public abstract function deleteMultiple(array $keys): bool;
public abstract function clear(): bool;
public abstract function lock(string $key, int $lease = 0): bool;
public abstract function unlock(string $key): bool;
public static function clearClass(string $class_name): bool;
public static function getCacheStoreType(string $key): CacheStoreType;
public static function getPropertyCacheStoreType(string
$class_name, string $property): CacheStoreType;
public abstract static function clearAll(): bool;
public abstract static function info(): StaticCacheInfo;
}
class VolatileCache extends Cache {
public function __construct(string $namespace = "");
// Note that set() and setMultiple() are redeclared with optional
`int $ttl = 0` parameters
public function set(string $key,
null|bool|int|float|string|array|object $value, int $ttl = 0): bool;
public function setMultiple(array $values, int $ttl = 0): bool;
public static function clearAll(): bool;
public static function info(): StaticCacheInfo;
}
class PinnedCache extends Cache {
public function __construct(string $namespace = "");
public function increment(string $key, int $step = 1): int|false;
public function decrement(string $key, int $step = 1): int|false;
public static function clearAll(): bool;
public static function info(): StaticCacheInfo;
}
Given that the API is nearly identical, we can simplify it for both
cache types and any library which actually requires $ttl can explicitly
require VolatileCache instead of PinnedCache - with just $ttl being
different.
An *application* author who does not want to carry around an instance of
the caches, can trivially write define('VOLATILE', new VolatileCache);
once and write VOLATILE->get("mykey") everywhere, getting the same
usability than Volatile::get("mykey"), essentially.
This will allow you to inject well-scoped caches (instead of relying on
the libraries to prefix their keys), declare new cache impls (e.g.
"class LocalCache extends Cache", which would just do request-local
caching rather than actually storing it) and make it easy to select
between PinnedCache and VolatileCache as a library user.
I'm pretty confident this is what a lot of people here want to actually
have, API wise.
Regarding atomic increment/decrement: does it actually matter that
volatile does not guarantee continuity?
The behaviour of missing key is well-defined. I consider it much better
to provide it than have users create their own poor-mans counter of
get() + set() combination.
Also, a counter on a VolatileCache may actually be useful - e.g. the
counter dropping back to zero is an indicator that eviction started. I
would not assume this useless. Less useful than a counter of a
PinnedCache, yes, but omitting it from VolatileCache is too opinionated.
Regarding Storable values: why are Closures not storable? The RFC says:
"Closure objects are request-local executable state and cannot be
represented as stable shared cache values"
But that's not quite true - all Closures, except for those from non-file
inclusions, like eval(), are effectively available in opcaches shared
memory. And those which are not, could technically be stored too - might
be a bit more expensive, but it's seldom the case. Would need some
custom serialization though. Not supporting them is a choice, but the
reason ("cannot") is the wrong one.
Regarding Attributes: Ah, the big contentious topic?
I'm not sure what to make of them. I get the appeal of a nice attribute
that makes it just work, but it's limited, in sort of subtle ways:
- Refreshing the values ... is impossible? The RFC text says manually
going through the Volatile/PinnedCache APIs with the
'volatile_static_class:' prefix and such is disallowed?
- Do we actually need to reserve these (*_static[_class]:) prefixes?
Can't we keep this an implementation detail and make sure that there are
dedicated API methods to properly handle it?
- Why do we need dedicated prefixes? (volatile_ and pinned_, that is)
Can't we look the specific class / property up and derive the required
cache internally?
- The tracking implementation works by adding a tracking callback to the
hot path of every array modification. Minor, but noticeable cost if
nothing uses these attributes. Significant cost if attributes are used.
Even more significant if writes are alternating (i.e. subsequent writes
access different arrays). That's a total non-starter. Maybe you could do
some hackery with overloading RW access to static properties
specifically to assume dirtied. But the current implementation is a
no-no. I get that this also affects stored objects and such. But, as
said, too invasive/expensive.
I really wonder whether you sat down at any point here and asked
yourself "should I really do this?", given all its complexity here.
- Scalar values are also not auto-refreshed, which probably makes for
surprising behavior when trying to increment some counter, and other
requests increment in parallel. I'm not too happy about the developer
experience around this.
Overall, the attributes probably need quite a bit more discussion and
refinement. I think it might be worth splitting it fully off into its
own RFC.
And the implementation will be probably also downsized by more than
half, making it a bit more manageable to review and assess.
The general integration of APCu-like capabilities with separate pinned
and volatile caches make a lot of sense to me.
With some minor refinements, I think I'd like to see this functionality
in PHP!
Thanks for your effort on this RFC,
Bob