Prune old revisions¶
SpecStar keeps every change as a new revision (see
Data versioning). That history is the
point — but on long-lived resources it also accumulates storage you may no
longer need. prune_revisions hard-deletes selected historical revisions to
reclaim space, while always keeping the current revision.
Python API only (for now).
prune_revisionsis aResourceManagermethod. There is no generated HTTP route and no permission gate for it yet — call it from trusted server-side code (a maintenance job, a CLI command, a scheduled task). It does emit apruneevent, so any event handler you have registered observes it like any other action.
The API¶
pruned_ids = mgr.prune_revisions(
resource_id,
*,
keep_last_n=..., # int >= 1, optional
before=..., # aware datetime, optional
user=..., # who performed the prune (for the event), optional
now=..., # clock for the event, optional
)
- Returns
list[str]— the revision ids that were pruned (empty when nothing matched). - Raises
ValueErrorif you pass neitherkeep_last_nnorbefore, or ifkeep_last_n < 1. - Raises
ResourceIDNotFoundErrorif the resource id does not exist. - Works on soft-deleted resources too.
You must supply at least one of keep_last_n or before; there is no
"prune everything" mode, because the current revision is never prunable.
What gets kept¶
Two knobs select what to keep; everything else is pruned.
keep_last_n keeps the n most important revisions, ranked from most to least
important by:
- Lineal first — revisions on the current revision's ancestry chain (follow
parent_revision_idfrom the current revision back to the root) rank above collateral branches. Collateral branches appear when youswitch()to an old revision and then edit, forking the history. - Newer first — within the same lineage, more recently created wins.
The current revision is always lineal and the newest of its own line, so it is
always rank 1 and never pruned. (A deterministic tie-break on the revision
sequence number keeps keep_last_n stable when several revisions share a
timestamp: the full sort key is (is_lineal, created_time, sequence), ranked
descending.)
before keeps every revision created at or after the given timestamp and
prunes those created strictly before it.
total_revision_countnever decreases. It seeds new revision ids and must stay monotonic, so pruning leaves it untouched. The live number of revisions islen(mgr.list_revisions(resource_id));mgr.get_meta(resource_id).total_revision_countis the running total ever created, which keeps climbing.
Example¶
Take a Config resource that has been edited many times. We stamp explicit
timestamps so the before example is reproducible.
from datetime import datetime, timedelta, timezone
import msgspec
from fastapi import FastAPI
from specstar import spec, Schema
class Config(msgspec.Struct):
value: str
app = FastAPI()
spec.configure(default_user="ops", default_now=lambda: datetime.now(timezone.utc))
spec.add_model(Schema(Config, "v1"))
spec.apply(app)
mgr = spec.get_resource_manager(Config)
# 8 revisions, one per day: rev 1 (oldest) ... rev 8 (current).
base = datetime(2025, 1, 1, tzinfo=timezone.utc)
info = mgr.create(Config(value="v0"), now=base)
rid = info.resource_id
for i in range(1, 8):
mgr.update(rid, Config(value=f"v{i}"), now=base + timedelta(days=i))
print(len(mgr.list_revisions(rid))) # 8 -> live revision count
print(mgr.get_meta(rid).total_revision_count) # 8 -> running total
Keep the last N¶
pruned = mgr.prune_revisions(rid, keep_last_n=3)
print(len(pruned)) # 5 -> the 5 oldest were pruned
print(len(mgr.list_revisions(rid))) # 3 -> current + 2 newest survive
print(mgr.get_meta(rid).total_revision_count) # 8 -> unchanged (monotonic)
The current revision is still readable, untouched:
Prune everything older than a date¶
On a fresh 8-revision resource:
cutoff = base + timedelta(days=4) # keep rev 5..8, prune rev 1..4
pruned = mgr.prune_revisions(rid, before=cutoff)
print(len(pruned)) # 4
print(len(mgr.list_revisions(rid))) # 4
Combine both knobs (conservative union)¶
When you pass both, the kept sets union: a revision survives if either
knob would keep it. Equivalently, a revision is pruned only when it is both
beyond keep_last_n and older than before.
# keep_last_n=2 alone would prune 6 (keep rev 7, 8).
# before=cutoff alone would prune 4 (keep rev 5..8).
# Union keeps rev 5..8 -> prunes only 4. rev 5, 6 survive because `before`
# keeps them even though keep_last_n=2 would have dropped them.
pruned = mgr.prune_revisions(rid, keep_last_n=2, before=base + timedelta(days=4))
print(len(pruned)) # 4
Blob interplay¶
If your resources carry binary data, pruning a
revision decrements the reference count of the blobs that revision pointed
at — it never deletes blob contents directly. A blob is only reclaimed once no
surviving revision references it, and reclamation happens in a separate
garbage-collection pass you run yourself with spec.gc(...).
So the storage from pruned revision payloads is freed immediately, but the
underlying blob bytes are freed on the next GC. See
Binary data → Blob lifecycle and garbage collection
for how and when to run gc().
Related pages¶
- Data versioning — how revisions are created and read in the first place
- Binary data — blob storage and the
gc()pass that reclaims unreferenced blobs - Event handlers — observe the
pruneevent