TAIL Retention / Garbage Collection
Bounds TAIL version-history growth via boundary-sentinel-preserving pruning โ built and tested, not yet auto-triggered.
Status: ๐ง Partial ยท Visibility: Internal ยท Category: Indexing
๐ฏ What it solves
Versioned Secondary Indexes append a TAIL entry every time an AllowMultiple
indexed field on a Versioned component gains or loses a value โ that's what makes temporal ("who held this value
at TSN T") queries possible. TAIL is append-only by design, so under sustained churn on a given key (a field that
keeps flipping between a small set of values, for instance) it grows without bound: nothing today reclaims entries
that no transaction can possibly still need. TAIL retention/GC is the reclamation half of that design โ it exists
as a tested primitive, but the trigger that would run it automatically isn't wired up yet.
โ๏ธ How it works (in brief)
Given a TAIL buffer and a retentionTSN (the oldest TSN any in-flight transaction could still query), pruning
groups entries by chain (the entity/key-value pairing a TAIL sequence belongs to). For each chain it discards every
entry older than the newest one at or below retentionTSN, keeping that single newest entry as a boundary
sentinel โ the fact a reader sitting exactly at the retention edge needs to know whether the chain was Active or
Tombstoned. A chain whose sentinel is a Tombstone with nothing newer than it is dropped entirely, sentinel
included, since there's no longer any reader that could land on it. Pruning rewrites the kept entries into a fresh
buffer rather than mutating in place.
๐ป Usage
[Component("Game.GuildMember", 1)] // Versioned by default
struct GuildMember
{
[Index(AllowMultiple = true)]
public long GuildId;
}
// Every commit that moves an entity between GuildId values appends a TAIL entry for the old
// and new value (see Versioned Secondary Indexes) โ no extra call needed for that part:
using (var tx = dbe.CreateQuickTransaction())
{
ref var m = ref tx.OpenMut(aria).Write(MemberArchetype.M);
m.GuildId = nextGuildId; // one more TAIL entry, every commit, indefinitely
tx.Commit();
}
// There is no application-facing Prune()/Compact() call today. TailGarbageCollector.Prune exists
// and is unit-tested in isolation, but nothing in the commit, checkpoint, or deferred-cleanup path
// invokes it yet โ so the entry appended above is never reclaimed for the life of the process.
// Once wired, pruning is designed to run automatically (no API call), the same way HEAD/TAIL
// maintenance itself requires none.
โ ๏ธ Guarantees & limits
- Boundary-correct today as a primitive โ for any
retentionTSNpassed in, the kept set always lets a reader at that exact TSN resolve Active/Tombstoned correctly; entries withTSN > retentionTSNare never discarded. - Dead chains collapse fully โ a chain whose last surviving state is a Tombstone with no later activity is removed entirely rather than leaving a permanent one-entry remnant.
- Not yet automatically triggered โ no
MinTSN-advance hook, checkpoint hook, or background scheduler calls it.DeferredCleanupManager(which already drives the analogous component-revision and content-chunk reclamation onMinTSN/tail-transaction-completion) has no TAIL GC integration today. - Unbounded TAIL growth in production today โ every value transition on an
AllowMultipleindexed field of aVersionedcomponent adds a TAIL entry that nothing currently removes; storage cost scales with field churn over the life of the process, not with retention need. - Compaction reallocates the buffer โ a successful prune returns a new TAIL buffer ID distinct from the input;
the caller is responsible for repointing the HEAD buffer's
TailBufferIdto it (the not-yet-built integration layer's job, not application code's). - Tested in isolation only โ unit tests drive
TailGarbageCollector.Prunedirectly against a TAIL buffer; no concurrency/stress coverage exists yet for pruning racing live writers or temporal readers. - No public API โ this is an internal engine primitive; there is nothing for application code to call, configure, or observe.
๐งช Tests
- VersionedIndexTests โ Phase 5
GC Testsregion:TailGC_PruneOldEntries_KeepsBoundarySentinel,TailGC_DeadChain_FullyRemoved,TailGC_NoOldEntries_NothingPrunedโ drivesTailGarbageCollector.Prunedirectly, in isolation from any auto-trigger
๐ Related
- Sibling feature: Versioned (HEAD/TAIL) Secondary Indexes for MVCC
- Source:
src/Typhon.Engine/Indexing/internals/TailGarbageCollector.cs - Source:
src/Typhon.Engine/Ecs/internals/DeferredCleanupManager.cs(analogousMinTSN-driven cleanup; TAIL GC is not yet integrated here)