2026年5月19日(火) 2:14 Larry Garfield <[email protected]>: > On Mon, May 18, 2026, at 6:00 AM, Go Kudo wrote: > > > Hi Larry, > > > > Thank you for the detailed feedback. > > > > I updated the RFC and the implementation to address the concrete API and > > documentation issues you pointed out. > > > > https://wiki.php.net/rfc/opcache_static_cache > > > > The main changes are: > > > > - `persistent_atomic_decrement()` now creates a missing key with -$step, > > matching `persistent_atomic_increment()`. > > +1 > > > - `volatile_lock()` and `persistent_lock()` now have matching > > `*_unlock()` > > functions. > > I should note that this is an excellent example of where context managers > and a `using` block would be helpful. :-) > > > - The lock APIs also accept an optional lease value, so abandoned > > builder > > reservations can expire even in persistent-worker environments. > > +1 > > > - The status APIs now return a read-only `OPcache\StaticCacheInfo` > > object > > instead of arrays. > > +1 > > Though it's not entirely obvious to me which property I would need to > check every single time I want to try storing to it. enabled? available? > What's the fully safe read/write code pattern here? > > > - The RFC now documents that both static-cache backends are scoped to > > the > > lifetime of the current OPcache static-cache shared-memory segment, > > and are > > not durable storage. > > Ah, that's a big and important distinction! What does "lifetime of the > current opcace static-cache shared memory segment" mean to developers who > don't read this list? :-) Does it mean "persistent" also goes away if you > restart FPM/Apache/whatever? Does that mean this is all basically useless > for CLI? Those should be made very clear in non-internals-speak. > > So really, the distinction is more whether there's a TTL and eviction > strategy, or if the eviction strategy is just "fall over and die." In that > case, I'm not sure if "persistent" is even the right name for that part of > the API, as it's not, well, persistent. > > > - The RFC now describes when `__serialize()` and `__unserialize()` are > > called, and > > that userland serialization keeps those object graphs off the fastest > > direct/shared-graph path. > > I want to make sure I follow here. > > class Test { > pubic string $a; > public string $b; > } > > $t = new Test(); > > persistent_store('t', $t); > > // Later > > $loadedT = persistent_fetch('t'); > > If Test does not have serialize/unserialize magic methods, then the object > is stored "as is" and $t === $loadedT. If it does implement those methods, > then it behaves like $loadedT = unserialize(serialize($t)); > > Is that correct? (Please clarify with examples in the RFC either way.) > > > - The publication rules for `VolatileStatic immediate` mode, > > `VolatileStatic > > tracking` mode, and `PersistentStatic` have been expanded. > > - `VolatileStatic tracking` is now documented as publishing at PHP > > request > > shutdown, not process shutdown, even under FPM/FrankenPHP/persistent > > workers. > > +1 > > > Some of the broader points are open questions or design tradeoffs rather > than > > direct fixes. > > > > On the default-off setting, I want to be more direct about where I > actually > > stand. I would prefer this feature to be default-on. Administrator opt-in > > noticeably limits library adoption, and that significantly reduces the > value > > of the API as a portable primitive that libraries can rely on being > present. > > > > What is holding me back is the shared-hosting case. The cache is a single > > shared-memory trust domain, so on a host where multiple tenants share one > > OPcache segment, default-on without an isolation story could expose those > > tenants to each other through the static cache. I do not yet have a > design I > > am confident makes default-on safe in that environment, and that is the > only > > reason the RFC currently ships with the feature disabled. > > > > I would genuinely appreciate your input here. If you (or anyone on the > list) > > see a viable path — per-pool / per-SAPI segments, a trust-domain or > namespace > > mechanism enforced by the engine, a configuration model where the host > opts > > in per vhost rather than per server, or something I have not considered > — I > > would be very happy to rework the proposal around it. If we can land on a > > model that is safe for shared hosting, I would gladly flip the default in > > this RFC rather than defer it to a follow-up. > > I suppose the question I'd ask here is how much of a factor is that these > days? Shared hosting was a huge part of PHP's early years, but... I don't > remember the last time I actually ran on a traditional shared hosting setup > with shared opcache between different tenants. How much of the market even > is that these days? (I have no idea.) > > > For disabled backends, the explicit APIs fail rather than silently > pretending > > to be a miss-only cache. Attribute-backed state falls back to ordinary > request > > local static behavior when the corresponding backend is unavailable. I > can add > > a short FAQ entry for this development-mode behavior if that would make > the RFC > > clearer. > > Yes please. > > > For multi-library key collisions, the explicit cache remains a shared > namespace > > and applications/libraries still need key prefixes, similar to APCu. The > new > > attribute deletion and exact-key deletion support should reduce the need > for > > whole-backend clears, but this RFC does not add a separate namespace > mechanism. > > I left the broader trust-domain/namespace question as an Open Issue, and > it is > > closely related to the shared-hosting question above. > > I don't have a good answer other than introducing "pools", which would > likely make the API much more involved. That could be good, though, I'm > not sure. (Esp. if it pushes the API toward OOP rather than global > functions.) > > > The memory-resident SQLite idea is interesting, but I think it is > outside the > > scope of this RFC. This proposal is intentionally a small key/value and > static > > state facility first. > > Oh totally. It's not something that belongs in this RFC directly. I just > want to see if there's a path to extend it from here in a future RFC to > enable such functionality. (I have no idea what that would look like off > hand.) > > --Larry Garfield >
Hi Larry, Benjamin, Thanks for the further round of comments. Benjamin, your Reddit pointer ended up being more useful than I first thought; I'll come back to it. I'll push RFC v1.2 shortly with the following changes. #### Renaming `PersistentStatic` → `PinnedStatic` (and the matching API and INI directive) You're right that "persistent" is misleading. The data is not durable in any disk-persistence sense; it lives only as long as the OPcache static-cache shared-memory segment, which is destroyed when its owner exits. "Pinned" captures the actual property: these entries are not evictable and have no TTL, but they only exist in memory. The new spellings are: - Attribute: `#[OPcache\PinnedStatic]` - API: `OPcache\pinned_store()`, `pinned_fetch()`, `pinned_lock()`, `pinned_unlock()`, `pinned_atomic_increment()`, `pinned_atomic_decrement()`, `pinned_cache_info()`, etc. - INI: `opcache.static_cache.pinned_size_mb` - Status object: `OPcache\StaticCacheInfo` for both backends (unchanged shape; only the accessor on `opcache_get_status()` is renamed) - Internal key prefixes: `pinned_static:` and `pinned_static_class:` - Exception: `OPcache\StaticCacheException` (unchanged) You're also correct that CLI use is mostly pointless for both backends. The pinned cache and the volatile cache both die at CLI process exit, the same way APCu and OPcache itself do today. The feature is aimed at long-lived SAPIs (FPM, FrankenPHP, PHP embed users, etc.) where the shared-memory owner outlives a single request, and the RFC now states that explicitly. #### Default INI values changed to 8 MiB each (was 0) After your default-off feedback and a closer look at how PHP is actually hosted today, I'm flipping the defaults. Both `opcache.static_cache.volatile_size_mb` and `opcache.static_cache.pinned_size_mb` will default to 8, which is the documented minimum. Administrators can still disable either backend by setting it to 0 explicitly. The shared-hosting concern that kept me on default-off turned out to be handled by the implementation, which I hadn't actually checked from that angle. The static-cache SHM is allocated through OPcache's existing `mmap(MAP_SHARED | MAP_ANONYMOUS)` handler in the FPM master's MINIT (or the equivalent SAPI startup point), before worker fork. The resulting mapping is anonymous, so unrelated processes cannot attach to it; only descendants of the FPM master that created it inherit access. So "one FPM master = one trust domain" is enforced by the kernel, not by the RFC. That lines up with how PHP is hosted today: - VPS, dedicated, and containerised single-tenant deployments are already one trust domain. - CloudLinux PHP Selector, the de facto standard for modern shared hosting, gives each user their own alt-php binary, so each user runs under their own master with their own SHM segment. - Per-pod / per-tenant Kubernetes or Docker deployments isolate the process tree and IPC, so there is no cross-tenant SHM. - Managed WordPress and similar managed-application platforms run each site under its own PHP process or container. The remaining edge case is the traditional cPanel default where multiple cPanel users share one ea-php-fpm master per PHP version. That configuration already has the same exposure for `opcache_get_status()`, which is why it is conventionally disabled there. The operational answer is the same: either set the static-cache backends to 0 in `php.ini`, or migrate to per-user PHP binaries via PHP Selector. I'll cover this in a migration-notes / shared-hosting section so admins running affected configurations have a clear instruction. Full disclosure: I haven't touched a multi-tenant shared host in nearly twenty years, so I'm not really in a position to judge the current landscape. I had assumed the legacy "all tenants share one PHP master" model was still common; it isn't, and PHP Selector has quietly become the standard while I wasn't paying attention. Thanks for pushing me to actually look it up. :-) Benjamin, your Reddit pointer to the APCu C-level pool-isolation patch was a useful reference point, even though the per-pool C-level hook approach itself is out of scope for this RFC. It also confirmed that the industry has settled on engine- or kernel-level isolation rather than userland prefix conventions, which lines up with the per-FPM-master story above. #### `StaticCacheInfo` "is this backend usable right now?" pattern Fair point. The current text doesn't make this obvious. v1.2 will include a polished `StaticCacheInfo` shape with proper documentation and the recommended check pattern. The field to test before any store/fetch attempt is `available`. It is `true` only when the backend is configured, started up successfully, and the SHM segment is initialised. `enabled` reports configured non-zero memory only and does not imply usability. A code example will accompany the property table. **`__serialize()` / `__unserialize()` behaviour** I'll add the concrete example you sketched to the Storable Values / serialization section. The summary in your reply matches the implementation: - A class with no userland serialization hooks goes through the fast shared-graph / direct-restore path. A successful round trip yields a freshly cloned but structurally equal graph, so the loaded value is `==`-equal to the stored one but, for object-bearing graphs, not `===`, because each fetch returns its own independent clone of the request-local prototype. - A class that defines `__serialize()` / `__unserialize()` is taken off the fast path. The semantics are equivalent to `unserialize(serialize($value))`: those hooks are called, but outside the cache read/write lock. ## Development-mode behaviour FAQ Will add. Disabled backends report `available = false` on the status object, so the recommended idiom (test `available` before storing) also covers the "framework wants to bypass cache in dev" case, since admins or dev-mode `.user.ini` overrides can set the size directive to 0 to disable a backend explicitly. Attribute-backed storage on a disabled backend falls back to ordinary request-local static behaviour, so code annotated with `#[OPcache\VolatileStatic]` or `#[OPcache\PinnedStatic]` keeps working on disabled-backend hosts; the attribute becomes a no-op. #### Multi-library key collisions I'll leave this in Open Issues for now. The class-name and exact-key deletion paths added in v1.1 already cover the most common "rebuild my library's keys" need without a full clear, but a true namespace/pool primitive is a larger design conversation than this RFC should take on. #### Memory-resident SQLite Agreed, that's well outside this RFC's scope. The static-cache backends use OPcache-managed SHM through the existing shared-memory handler abstraction, so in principle other engine-internal subsystems could one day reuse that infrastructure for a memory-resident SQLite or similar. That's a topic for a future RFC; I'll note it in Future Scope without committing to it. Best regards, Go Kudo
