Pure accounting core for the layer GC.
A Hyper.Img.Db.Gc.Sweep.State accumulates a single full pass over the blobs
table. absorb/3 classifies a keyset page into present (file on the medium) vs
missing (file gone) and returns the missing blobs for the GC to prune;
record_prune/4 folds the prune outcome back into the tally once the GC has
executed it against the database.
Summary
Types
A blob page row: content-addressed id and its DB-recorded byte size.
Injected shared-medium probe. :present = file is there; :missing = file is
genuinely gone (safe to prune); :unknown = could not determine (I/O error),
so the blob is counted but never pruned.
Functions
Fold one keyset page into the sweep. Advances cursor to the last blob's id
(so the next page starts after it), counts present / missing / unknown, and
returns only the genuinely-missing blobs (file confirmed absent) as {id, size}
for the GC to prune. :unknown rows (probe I/O error) are counted but never
returned, so an unreachable medium can never drive a delete.
Full page (length == limit) means more rows may remain; page again.
A fresh sweep state: nil cursor (start of table), zeroed counters.
Fold the result of pruning one page back into the sweep: pruned rows deleted
totalling pruned_bytes, and dangling rows left in place (file gone but the
blob is still referenced by an image - a data-loss condition the GC reports but
must not delete).
Types
@type blob() :: {String.t(), non_neg_integer()}
A blob page row: content-addressed id and its DB-recorded byte size.
@type presence() :: :present | :missing | :unknown
Injected shared-medium probe. :present = file is there; :missing = file is
genuinely gone (safe to prune); :unknown = could not determine (I/O error),
so the blob is counted but never pruned.
Functions
@spec absorb(Hyper.Img.Db.Gc.Sweep.State.t(), [blob()], check_fun()) :: {Hyper.Img.Db.Gc.Sweep.State.t(), [blob()]}
Fold one keyset page into the sweep. Advances cursor to the last blob's id
(so the next page starts after it), counts present / missing / unknown, and
returns only the genuinely-missing blobs (file confirmed absent) as {id, size}
for the GC to prune. :unknown rows (probe I/O error) are counted but never
returned, so an unreachable medium can never drive a delete.
@spec continue?([term()], pos_integer()) :: boolean()
Full page (length == limit) means more rows may remain; page again.
@spec new() :: Hyper.Img.Db.Gc.Sweep.State.t()
A fresh sweep state: nil cursor (start of table), zeroed counters.
@spec record_prune( Hyper.Img.Db.Gc.Sweep.State.t(), non_neg_integer(), non_neg_integer(), non_neg_integer() ) :: Hyper.Img.Db.Gc.Sweep.State.t()
Fold the result of pruning one page back into the sweep: pruned rows deleted
totalling pruned_bytes, and dangling rows left in place (file gone but the
blob is still referenced by an image - a data-loss condition the GC reports but
must not delete).