Skip to content

Port v0.16.1-rc.3 / v0.16.1 to main#671

Merged
turbolent merged 22 commits into
mainfrom
v0.16
Jul 2, 2026
Merged

Port v0.16.1-rc.3 / v0.16.1 to main#671
turbolent merged 22 commits into
mainfrom
v0.16

Conversation

@turbolent

@turbolent turbolent commented Jul 2, 2026

Copy link
Copy Markdown
Member

  • Targeted PR against main branch
  • Linked to Github issue with discussion and accepted design OR link to spec that describes this work
  • Code follows the standards mentioned here
  • Updated relevant documentation
  • Re-reviewed Files changed in the Github PR explorer
  • Added appropriate labels

turbolent and others added 22 commits May 29, 2026 11:55
Multiple Go-level *Array or *OrderedMap instances created over the same
logical container held independent root slab pointers.
A structural change (splitRoot, promoteChildAsNewRoot) performed through
one instance left siblings observing stale roots.

Hoist the mutable state onto a shared *arrayState / *orderedMapState
that lives in a per-SlabStorage registry keyed by root SlabID.
All instances over the same logical container share one state pointer,
so structural changes propagate automatically.

parentUpdater stays per-instance: a regular Get installs a real
parent-notification callback while a readonly iterator installs
a trap callback; two siblings can legitimately have different parentUpdaters
and a mutation must fire only the originating instance's callback.

SlabStorage gains a state registry. External implementations can
embed *BaseStateRegistry to satisfy them with zero boilerplate.
BasicSlabStorage and PersistentSlabStorage do exactly this.
Eliminate stale-sibling-root inconsistency
A registered container state deliberately survives storage.Remove
because Remove is also called when a container is inlined while still alive.
The side effect: after a container was genuinely destroyed,
the constructors served the leftover state as a live container (a zombie),
where they previously returned SlabNotFoundError.

Distinguish the two cases on lookup:
a non-inlined root must still have its slab in storage.
If the slab is gone, the container was destroyed —
clear the leftover state and return SlabNotFoundError,
restoring pre-shared-state behavior.
Inlined roots are exempt since they legitimately have no standalone slab.

The existence check only fires on a registry hit with a non-inlined root;
a cold-cache hit costs one base-storage read + decode,
which is what the constructors paid on every call
before the shared-state registry existed.

Adds StateRegistry.RemoveStateForSlab so the constructors can clear
the stale entry through SlabStorage;
BaseStateRegistry already implements it, so embedders are unaffected.
When the shared-state registry was introduced,
NewMapWithRootID lost the check that the slab carries extra data;
its array counterpart kept it.
Only root slabs carry extra data —
a slab without it is an interior slab, not a value.

Without the check, passing an interior slab ID
silently produced a broken *OrderedMap with an unseeded digester,
where it previously returned NotValueError.
Worse, the bogus state was registered in the registry under that slab ID,
poisoning every later lookup for it.

Reject the slab before registering state,
so a failed call leaves the registry untouched.

The new tests cover both container types:
- the map test is the regression test,
- the array test pins the existing check against the same mistake.
DropDeltas rolls storage back to the last committed state,
but registered container states point at in-memory root slabs
that still reflect the discarded mutations.
An *Array / *OrderedMap instance created after the rollback
would be served such a state from the registry
and resurrect the discarded writes.

Add BaseStateRegistry.RemoveAllStates and call it from DropDeltas,
so post-rollback instances re-decode the committed slabs.

Note that DropDeltas alone still leaves
in-place-mutated slab structs in the read cache;
callers wanting a full reset to committed state
pair it with DropCache (as cmd/smoke does),
and the new test follows that pattern.
DropDeltas cleared deltas and the state registry,
but slabs are mutated in place
and the same mutated structs also sit in the read cache
(commit places stored slab structs in the cache,
and cached reads can be mutated afterwards).
A fresh container load after the rollback
would read the mutated struct from the cache
and observe — and on a later commit re-persist —
the supposedly discarded mutations,
so DropDeltas alone was not a complete rollback
and silently required pairing with DropCache.

Every mutated slab has a delta entry,
so invalidating the cache for all delta'd IDs
makes subsequent reads decode the committed bytes again.
DropDeltas alone is now a complete rollback
for everything storage serves afterwards.

One vector remains and cannot be closed in atree:
container instances obtained before the rollback
still hold their mutated in-memory roots.
Document on DropDeltas that callers must discard
retained container references across a rollback.

The test now deliberately omits DropCache
and asserts mutated slabs are no longer served from memory.
NewArrayWithRootID and NewMapWithRootID only prove access to
standalone root slabs.
After a container is inlined into its parent,
the old root ID no longer names a live standalone slab,
even though the shared-state registry must keep the state alive
for parent-loaded siblings.

Reject constructor calls that hit an inlined registered state
without clearing that state.
Also mark root-ID-loaded wrappers and reject later mutations through them
if another sibling has since inlined the container
and the wrapper has no parent updater.

This prevents silent in-memory mutations that cannot be written back
to the parent while leaving normal parent-loaded and standalone Cadence paths
intact.

Add array and map regressions covering existing root-ID handles,
fresh root-ID loads after inline,
registry retention,
and continued mutation through the parent-loaded handle.
Co-authored-by: Leo Zhang <zhangchiqing@gmail.com>
Reject root-ID access to inlined containers
@turbolent turbolent changed the title Port v0.16.1-rc.3 to main Port v0.16.1-rc.3 / v0.16.1 to main Jul 2, 2026
@turbolent turbolent merged commit a99ffff into main Jul 2, 2026
6 checks passed
@turbolent turbolent deleted the v0.16 branch July 2, 2026 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants