test(e2e): cover the three storage-counter rows
What this adds
The three catalog rows for storage counters, taking the oci and docker catalogs
to 45 covered, 0 disagreements:
| Scenario | Priority |
|---|---|
e2e.oci.lifecycle.repository-storage-counters |
high |
e2e.oci.lifecycle.delete-storage-counters |
high |
e2e.docker.lifecycle.delete-storage-counters |
high |
All three pass against a caproni bootstrap rig. Full suite: 102 tests, exit 0.
Neither counter column is written by the request that changes it: both are deltas on a buffered pipeline that a periodic drain writes. Every read here polls, and the arms that assert a column does not move wait long enough for a drain to have landed had there been a delta.
registry.Client gains the two counter columns on Repository, DeleteBlob, and
the three management deletes these rows need — DeleteContainerManifest,
DeleteContainerTag and BulkDeleteContainerManifests — with the shared image
route built in one place.
The reference model, which is the point of the change
container_blobs is keyed on (namespace_id, container_image_id, digest), so a
blob reference is per image, not per manifest. Two manifests under one image
share one reference row, and deleting it frees the blob at once. The catalog's
"only once the repository holds no other reference to it" is therefore observable
only with two images sharing a digest: the bytes stay while the second image
holds a row, and go when it does not. Verified to the byte — a 1000-byte shared
layer holds at the first delete and drops by exactly 1000 at the second.
I got this wrong first, read it as a manifest naming the digest, and was one
instruction away from filing a bug against
S12's Blob Delete, which states in bold
that blob delete checks no manifest reference — by design, matching the OCI
Distribution Spec. The behaviour and the counter were both correct; the test was
not. e2e/README.md now carries the model under "A blob reference is per image,
not per manifest" so the next reader does not repeat it.
The delete journey
One journey per format, covering every delete the two rows name:
| Operation | artifacts_count |
size_bytes |
|---|---|---|
| Untag | unchanged | unchanged |
| Management manifest delete | −1 | − payload |
Repeat, answering 404 |
unchanged | unchanged |
| Bulk delete entry | −1 | − payload |
| Image delete | falls to 0 | holds until the reap |
Both rows share that journey in one file. They are both API rows and differ only in seeding — the docker arm builds an image with crane, because a docker repository refuses a generic artifact as primary content, while the oci arm pushes with oras. Written twice they would drift, which is what the mirrored-suite guardrail is about.
Two statuses came from the contract after I asserted both wrong: an untag and a
bulk delete each answer 202.
The rig change
test/caproni lowers drain_interval from thirty seconds to two, and
drain_chunk_stale_timeout with it, since it has to stay below the interval. The
counter row has four checkpoints; at the default it would spend two minutes
waiting. Both values files are updated, per the sync note their headers carry.
Notes for the reviewer
- Stacked on !2130 (merged), which carries the four container clients these rows use.
Target that, not
main. - 746 insertions, within the reviewable line, which is why this is separate from !2130 (merged) rather than another batch on top of it.
- !2129 (merged) also edits
e2e/internal/catalog/patterns.goand the two catalogs, so whichever of the three lands last will need a hand-merge there. The entries themselves do not overlap. - Adjacent but not overlapping: #1018 (closed) covers counter deltas on remote DELETE routes; these rows are hosted.