feat: key persisted graphs by name and add per-instance graphs - #23
Merged
Conversation
The registry always stored entries under an arbitrary string; only
PersistentStore's public surface pinned that string to a class name,
which made a second persist() of one class an upsert - the superseded
graph released while a sibling process might still be reading it by
address. The class-name key turns out to be pure API convention, not a
load-bearing property of the frozen store, so the convention is now
stated as what it is:
- persist($name, $object) keys by NAME. Passing ::class remains the
convention for a typed singleton and keeps get(AppConfig::class)
inferring its type (conditional return), but two instances of one
class live happily under two names, and the instanceof coupling
between key and object is gone.
- persistInstance($object) persists a graph under a name minted from
its own root address ('@' + hex). Instance graphs are many-per-class
by construction: none upserts another, any number are live at once,
and re-persisting an already-shared root is idempotent through the
ordinary upsert accounting. The '@' prefix is reserved so a chosen
name can never collide with a minted one.
- dropInstance($objectOrAddress) closes the loop; it accepts the
address form because the frozen store's alias check rightly counts
the argument itself as a live reference.
SharedError::capture() now uses persistInstance(), so a second panic
no longer supersedes the first: two workers failing near-simultaneously
each leave an error their waiter can still attach by address. The cost
is three short strings per panic until family teardown.
This is the substrate half of
lisachenko/native-php-coroutines#15; the runtime half lifts its
one-task-per-class refusal on top of it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5QRWPfmkZME7hDjU6i5EA
lisachenko
marked this pull request as ready for review
August 16, 2026 07:57
This was referenced Aug 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The substrate half of lisachenko/native-php-coroutines#15 (two concurrent tasks of one class cannot be in flight at once — Option 1, per-instance keys upstream).
The finding
The registry always stored entries under an arbitrary string name; only
PersistentStore's public surface pinned that string to a class name, andattach()walks the address-keyed object table — so the class key was API convention, not a load-bearing property of the frozen store. This answers the question #15 hedged on, in the direction its follow-up comment predicted.What changed
persist($name, $object)keys by name.::classremains the convention for a typed singleton — a conditional return type keepsget(AppConfig::class)inferring — but the key carries no class semantics: two instances of one class live under two names, and theinstanceofcoupling between key and object is gone.persistInstance($object)persists a graph under a name minted from its own root address ('@' + hex). Instance graphs are many-per-class by construction: none upserts another, any number are live at once, and re-persisting an already-shared root is idempotent through the ordinary upsert accounting (member share counts net out unchanged). The'@'prefix is reserved inpersist()so a caller-chosen name can never collide with a minted one and silently release a graph another process reads by address.dropInstance($objectOrAddress)closes the loop. It accepts the address form deliberately: the frozen store's alias-safety check rightly counts the argument itself as a live reference, so frozen-mode callers takeaddressOfInstance(), release their references, and drop by number. Arena-backed stores skip the alias predicate, so the instance form is fine there.SharedError::capture()usespersistInstance()— one entry per panic. A second panic no longer supersedes the first, so two workers failing near-simultaneously each leave an error their waiter can still attach by the address its own slot carries. Cost: three short strings per panic until family teardown, the arena's ordinary leak-until-teardown economics.Internally,
persist()'s pipeline split intoconvert()(graph → persistent memory + role stamping) andstoreUnder()(upsert registration), shared by both entry points. The registry itself needed no change.Verification
OK (170 tests, 14473 assertions)under PHP 8.4 (z-engine8.4.x-dev) and PHP 8.5 (dev-master),ffi.enable=1,opcache.jit=off.testKeyMustNameAClassOfTheInstancepinned the droppedinstanceofcoupling.SOAK OK(flat memory),SOAK-DROP OK(4.42 kB/cycle against a budget of 6).Noted, out of scope
Registry writes from two processes at once (a parent persisting a task while a worker persists a panic) are unlocked today; that predates this change and deserves its own issue rather than a ride-along fix — persist allocates far too much to run under an arena mutex, so it needs a design, not a lock.
🤖 Generated with Claude Code
https://claude.ai/code/session_01U5QRWPfmkZME7hDjU6i5EA
Generated by Claude Code