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

Reply via email to