Expiry is two decisions, in this order:
- Which snapshots go. Never the current one, never one a branch or tag
names, and never so many that fewer than
keep_last are left. Of what
remains, the ones older than older_than_ms.
- Which files go with them. A file is deleted only when no retained
snapshot still lists it live. That is stronger than "it was in an expired
snapshot": a data file added five snapshots ago is in every snapshot since,
and expiring the oldest of them must not touch it. So the retained
snapshots are walked first and every manifest list, manifest and live entry
they name is put in a keep set; only then is the expired snapshots' own
inventory subtracted from it.
A DELETED entry does not protect a file — that is the entry that says it is
gone — but an older retained snapshot that still lists it ADDED or
EXISTING does, which is why the keep set is a union over all of them.
The new metadata is written before any file is deleted. A crash in between
leaves orphans, which cost storage; the other order leaves a snapshot pointing
at files that are not there, which costs the table. dry_run stops after the
decision and returns the list, which is the only safe way to find out what an
expiry would do.
Metadata files (*.metadata.json) are left alone. Java deletes them under
write.metadata.delete-after-commit.enabled with its own retention count, and
that is a separate policy from snapshot expiry: the metadata log is how a
reader reaches a snapshot by timestamp in the first place.