Skip to content

The registry room guard refuses inserts a tombstoned table could absorb, so dropping entries never returns capacity in arena mode #28

Description

@lisachenko

The substrate half of lisachenko/native-php-coroutines#34 (instance-graph reclamation). Identified during the PR #23 design pass and deferred; the motivating workload has since been measured: the coroutines slot-recycling session reproduced sustained panics exhausting the default 256-entry entries table on unmodified main — one SharedError instance graph per panic, ArenaException::registryTableFull as the wall.

Today's behaviour

Registry::assertRegistryRoom() refuses any non-upsert insert into an arena-resident registry table once nNumUsed == nTableSize. That conservatism is correct as far as it goes — an engine growth would perealloc() shared storage into one process's private heap and silently unshare the registry — but it is tombstone-blind: zend_hash deletion marks buckets IS_UNDEF without decrementing nNumUsed, so a table that has seen drop() / dropInstance() traffic reports full while holding reclaimable slots. Net effect: dropping entries buys registry hygiene but no capacity, and a long-running family still hits the wall at (table size) total persists, not live ones.

The provable branch

The engine's own resize decision, zend_hash_do_resize():

if (ht->nNumUsed > ht->nNumOfElements + (ht->nNumOfElements >> 5)) {
    zend_hash_rehash(ht);          /* compacts IN PLACE — no reallocation */
} else {
    /* doubles the table — the forbidden path */
}

Both operands are readable from the table header at insert time, so the guard can mirror the arithmetic exactly: allow the insert when nNumUsed - nNumOfElements > nNumOfElements >> 5 (the in-place rehash is guaranteed, storage never moves), refuse otherwise as today. zend_hash_rehash memsets the hash area and compacts buckets within the existing block — arena-safe by construction. The logic is algorithmic, not offset-based, and stable across both supported minors; assertArenaResident() remains the after-the-fact tripwire if the mirror ever drifts from the engine.

Scope notes

Acceptance criteria

  • persist → drop → persist cycled (table size × 4) times on a pre-sized arena registry completes; entries plateau at the live count.
  • The guard still refuses when tombstones are below the engine's compaction threshold — asserted by a test that fills a table with live entries and shows the refusal unchanged.
  • A mirrored-branch unit test compares the guard's decision against the engine's actual behaviour (insert into a scratch heap table with the same counters and observe rehash vs. grow), so an engine-side change of the threshold fails a test rather than unsharing a registry.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions