Skip to content

A record at an object key holds a digest, not the object - #219

Merged
matrixarkai merged 1 commit into
mainfrom
perf/a-record-at-an-object-key-holds-a-digest
Oct 24, 2026
Merged

matrixarkai merged 1 commit into
mainfrom
perf/a-record-at-an-object-key-holds-a-digest

Conversation

@matrixarkai

Copy link
Copy Markdown
Owner

The metadata service kept its own rendering of every object — 227 characters, packed into a
64-byte struct and rendered on demand. The object itself is the authority on those characters,
so the record holds an eight-byte digest of them instead, and proves agreement without
carrying a second copy.

was now
meta.records.value_capacity 64 0
the metadata service's share 192 128
the metadata line 410 346 (fixed term unchanged at 164,256)

What this gives up, stated first

A restore is no longer self-sufficient. A snapshot carries the digest and not the
characters, so a restored metaserver can verify that a record matches an object and cannot
reproduce the value without the object. That is the trade, and it is why this is a migration
rather than a representation change.

What it keeps is the proof property

append_metadata_record(out, record) is still a pure function of a record, with no store in
reach. That is what the 64 bytes were buying: if an object record rendered its text by
consulting the storage node, a checksum over a set of records would become a function of the
records and one replica's current state — and two replicas differing by one in-flight write
would hash different material for identical records. A digest is computed where the record is
written, so the material stays a function of the record alone.

Sixteen bytes, measured rather than hoped for

A bucket, lifecycle or multipart record cannot hold a digest — its text is the only copy
there is — so the value has to hold either shape. The question was whether an enum over a boxed
string and a u64 stays sixteen bytes or grows to twenty-four.

It stays sixteen: a Box<str> is a non-null fat pointer, and the niche absorbs both the
discriminant and the digest's own eight bytes. Measured by
measure_the_route_to_eighty_bytes_a_metadata_record, which also ruled out a hand-rolled
tagged pair of words — the same sixteen, and it needs unsafe to read back.

The durable format states the shape, and does not infer it from the key

The obvious encoding is to let the key decide: a record under object/ holds a digest. That is
wrong here, and quietly — meta_object_key returns a bucket key for an object whose key is
empty
, so an object-keyed test misreads exactly those records. Both encoders write a u64
shape tag before the value's characters and RecordValue::from_kind_tag reads it back.

parse_etag_digest is new, beside the writer that spelled the digest. A spelling with a writer
and no reader is a one-way door; this one now opens both ways, and refuses a tag it does not
know or a prefix it cannot parse rather than reading them as the digest zero.

And the stamp, which I nearly shipped without

The decoder accepts MOSRSMETA versions 1..=16 and gates each past format addition on
version >= n. Adding a u64 without bumping the stamp would have left the new decoder
accepting a snapshot written before this change and reading the string length in front of
the value as the shape tag. A round-trip test cannot see that — it writes and reads with the
same build, so both halves are wrong together and agree.

So the stamp is MOSRSMETA17, the range is 1..=17, and has_record_value_kind = version >= 17
reads the tag only when it is there. An older snapshot decodes as it always did: its object
records come back as text — the full rendering, which is what it actually carries — and the
new mixed equality makes that equal to the digest a live write produces. So an old snapshot
restores correctly, and its records become digests as their objects are next written. The bump
swept three literal pins and the constant inventory.

the_snapshot_decoder_accepts_the_version_before_this_one_and_refuses_the_one_after pins the
range: 16 gets past the magic, 18 is refused, and so are MOSRSMETA0, MOSRSMETA017 — which
parses as seventeen and is not its own spelling — and another format's stamp at the same
version.

It does not prove an old snapshot decodes. There is no stored snapshot fixture in this tree,
for this version or any of the sixteen before it. That is a hole in the fixtures, and it is
worth naming rather than implying coverage this change does not have.

The size accounting was eight bytes short, and the suite caught it

metadata_record_value_encoded_size counted three u64 where the encoder now writes four. I
left it wrong for one run to find out whether the accounting is guarded at all: three
chunked-snapshot tests failed
, one with metadata snapshot string is too large. It is
guarded. The count is now four.

And one writer I missed, which is why the reader found it: append_metadata_record is
both the durable encoding for a receipt's embedded records and the material a proof hashes.
Changing its reader without it produced garbage — the honest signal that those two uses belong
in one function, because a proof's material and a snapshot's bytes should be the same function
of a record.

What the tests said before, and what they say now

Three tests asserted record.value.as_text().contains("version=1"). That asserts the
representation; the only thing a record can honestly assert now is agreement — the
digest it carries is the digest of the object it names. Stronger either way:
contains("version=1") also passes for a record describing a different object that happens to
be at version one.

the_record_value_conversion_is_free_after_a_snapshot_load_and_better_before_it is now
an_object_record_owns_no_heap_before_or_after_a_snapshot_load, and its claim is an equality
where it used to be an inequality. The projection shrank a live store and went back to text on
load, so the old test could only assert the loaded form was no worse. A digest survives, so the
occupied bytes are asserted equal before and after a round-trip — the only thing standing
between a correct decoder and one that reads the digest's twenty-four characters back as a boxed
string, giving the whole saving away on every restore while every live figure still looked right.

Both halves are asserted against the baseline rather than against each other: two stores
that had both rebuilt text would agree with each other too.

Three new guards, for the things a byte figure cannot see

a_digest_equals_the_text_it_is_a_digest_of_and_refuses_a_corrupt_tag — the listing audit
renders a projection from the live object and compares it to the stored record, so a digest
must equal the text it is a digest of. Going through as_text instead would read a correctly
written object as drifted; that exact bug happened once before, when this equality was derived
instead of written. It also asserts the shape survives the tag and not just the comparison,
and that five corrupt spellings are refused.

two_records_spelling_the_same_thing_in_different_shapes_hash_differently — a digest and its
own spelling held as text are equal and hash differently, because the material states the
shape first. Written down because the obvious tidy-up — dropping the tag so equal values hash
alike — would make a corrupt snapshot indistinguishable from a valid one.

And a guard from one change ago, pointed at what now matters

a_projection_holds_nothing_for_an_object_under_no_hold asserted that an object under no
restore, retention or legal hold owed no heap for them, because a stored projection charged
the audit for its box. Nothing stores a projection, so there is no box — and holds_heap_bytes,
added one change ago for exactly that figure, is dead code. The build under denied warnings is
what said so.

What remains matters more than the figure did. The three holds are three of the twelve rendered
fields, so they have to reach the digest. If legal_hold() ever returned a constant,
placing a legal hold would leave the record's value unchanged — the object's own copy would
still be right, every byte figure would still be right, and the two services would silently
disagree about an object under a hold. The old guard could not have caught that.

So it is now a_hold_reaches_the_digest_and_an_object_under_none_spells_zeros, and it flips
one field on a clone of one object for each of the three holds. That isolation is exact
because the rendered text carries no object name, so two metadata values differing in a hold
differ in nothing else. It also drives the write path: placing a legal hold through the store
must move the stored record's value, not just the renderer's output.

Three measurements retired, and one that lost a leg

Retired because their subjects are gone, not because they broke: the one pricing the
projection's twelve fields, the one pricing how much of a record's text was name=value
scaffolding, and the one asking whether a snapshot's text could be packed back into twelve
values. All three are #[ignore]d, so nothing runs them — they would have sat printing zeros.

measure_what_a_one_word_text_handle_would_save counted a text handle inside the stored
projection
. Nothing stores a projection, so that leg is not a saving available to anyone: 64
inline became 56, and the published nets 48/32 became 40/24. A mirror of a type that stops
being resident publishes a saving nobody can collect — the third stale mirror this file has
produced.

Where the remaining forty bytes are

128  today
120  the deleted flag into the version's top bit -- one bit in its own 8-byte word
104  and the duplicate key handle dropped: the map and the record hold one allocation twice
 88  and the key's characters owned rather than shared -- the one rung that carries a
     trade: one copy per retained change, 40,960 bytes of fixed term, pays above 2,560 objects

Below 88 is the object's own name and one handle to it. Eighty needs the records map split
by kind
, so an object record's value slot can be eight bytes instead of sixteen — the slot is
sixteen because a bucket record shares the map and its text is the only copy there is.

Also, a mangled message from the change before this one

a_storage_class_is_one_byte_and_its_wire_is_still_a_name shipped with ten spaces inside an
assertion message. The hygiene check flagged it and I read past it.

The figures, each re-measured

amplification rows 4 KiB 3.11x → 3.09x; 1 KiB 3.44x → 3.38x
striped against replicated 2.09x → 2.03x; 3.58x → 3.51x
metadata an object at 100 / 1,000 / 4,000 / 8,000 554 / 510 / 387 / 366
delete trade holding 1,799,649, emptied 2,268,662, 25.6% → 26.0%
the share table objects 210 (60.7%), records 128 (37.0%), keys 8 (2.3%)

The feeds' share of metadata was published as two percentages of a total that just moved; it is
now the 163,866 bytes they hold at their bound, a figure the line guard already pins.

Measured

  • Suite: 411 passing on the default feature set, 432 with every feature, the same 21 failures in both and name for name the baseline in tools/known_failures.txt
  • cargo build --all-targets --locked and --all-features, RUSTFLAGS=-D warnings — green
  • cargo doc --no-deps --locked and --all-features, RUSTDOCFLAGS=-D warnings — green

🤖 Generated with Claude Code

The metadata service kept its own rendering of every object: 227 characters,
packed into a 64-byte struct and rendered on demand. The object itself is the
authority on those characters, so the record holds an eight-byte digest of them
instead and proves agreement without carrying a second copy.

  meta.records.value_capacity   64 -> 0
  the metadata service's share 192 -> 128
  the metadata line            410 -> 346, fixed term unchanged at 164,256

WHAT THIS GIVES UP, STATED FIRST

A restore is no longer self-sufficient. A snapshot carries the digest and not
the characters, so a restored metaserver can VERIFY that a record matches an
object and cannot reproduce the value without the object. That is the trade, and
it is the reason this is a migration rather than a representation change.

WHAT IT KEEPS IS THE PROOF PROPERTY

`append_metadata_record(out, record)` is still a pure function of a record with
no store in reach. That is what the 64 bytes were buying: if a record rendered
its text by consulting the storage node, a checksum over a set of records would
become a function of the records AND one replica's current state, and two
replicas differing by one in-flight write would hash different material for
identical records. A digest is computed where the record is written, so the
material stays a function of the record alone.

SIXTEEN BYTES, MEASURED RATHER THAN HOPED FOR

A bucket, lifecycle or multipart record CANNOT hold a digest -- its text is the
only copy there is -- so the value has to hold either shape, and the question was
whether an enum over a boxed string and a `u64` stays sixteen bytes or grows to
twenty-four. It stays sixteen: a `Box<str>` is a non-null fat pointer and the
niche absorbs both the discriminant and the digest's own eight bytes. Measured by
`measure_the_route_to_eighty_bytes_a_metadata_record`, which is also what ruled
out a hand-rolled tagged pair of words -- the same sixteen, and it needs `unsafe`
to read back.

THE DURABLE FORMAT STATES THE SHAPE, AND DOES NOT INFER IT FROM THE KEY

The obvious encoding is to let the key decide: a record under `object/` holds a
digest. That is wrong here, and quietly. `meta_object_key` returns a BUCKET key
for an object whose key is empty, so an object-keyed test misreads exactly those
records. Both encoders write a `u64` shape tag before the value's characters and
`RecordValue::from_kind_tag` reads it, so nothing has to guess.

`parse_etag_digest` is new beside the writer that spelled the digest. A spelling
with a writer and no reader is a one-way door, and this one now opens both ways
-- including refusing a tag it does not know and a prefix it cannot parse, rather
than reading them as the digest zero.

AND THE STAMP, WHICH I NEARLY SHIPPED WITHOUT

The decoder accepts `MOSRSMETA` versions 1..=16 and gates each past format
addition on `version >= n`. Adding a `u64` without bumping the stamp would have
left the new decoder ACCEPTING a snapshot written before this change and reading
the string length in front of the value as the shape tag. A round-trip test
cannot see that: it writes and reads with the same build, so both halves are
wrong together and agree.

So the stamp is `MOSRSMETA17`, the range is 1..=17, and
`has_record_value_kind = version >= 17` reads the tag only when it is there. An
older snapshot decodes as it always did -- its object records come back as TEXT,
the full rendering, which the new mixed equality makes EQUAL to the digest a live
write produces. So an old snapshot restores correctly and its records become
digests as their objects are next written. The bump swept three literal pins and
the constant inventory.

`the_snapshot_decoder_accepts_the_version_before_this_one_and_refuses_the_one_after`
pins the range: 16 gets past the magic, 18 is refused, and so are `MOSRSMETA0`,
`MOSRSMETA017` -- which parses as seventeen and is not its own spelling -- and
another format's stamp at the same version. It does NOT prove an old snapshot
decodes: there is no stored snapshot fixture in this tree, for this version or
for any of the sixteen before it. That is a hole in the fixtures and it is worth
naming rather than implying coverage this does not have.

THE SIZE ACCOUNTING WAS EIGHT BYTES SHORT, AND THE SUITE CAUGHT IT

`metadata_record_value_encoded_size` counted three `u64` where the encoder now
writes four. Left wrong on purpose for one run to find out whether the accounting
is guarded at all: three chunked-snapshot tests failed, one of them with
`metadata snapshot string is too large`. It is guarded. The count is now four.

AND ONE WRITER I MISSED, WHICH IS WHY THE READER FOUND IT

`append_metadata_record` is BOTH the durable encoding for a receipt's embedded
records and the material a proof hashes. Changing its reader without it produced
garbage, which is the honest signal that the two belong in one function: a proof's
material and a snapshot's bytes should be the same function of a record.

WHAT THE TESTS SAID BEFORE, AND WHAT THEY SAY NOW

Three tests asserted `record.value.as_text().contains("version=1")`. That asserts
the REPRESENTATION, and the only thing a record can honestly assert now is
AGREEMENT: the digest it carries is the digest of the object it names. Stronger
either way -- `contains("version=1")` also passes for a record describing a
different object that happens to be at version one.

`the_record_value_conversion_is_free_after_a_snapshot_load_and_better_before_it`
is now `an_object_record_owns_no_heap_before_or_after_a_snapshot_load`, and its
claim is an EQUALITY where it used to be an inequality. The projection shrank a
live store and went back to text on load, so the old test could only assert the
loaded form was no worse. A digest survives, so the occupied bytes are asserted
equal before and after a round-trip -- which is the only thing standing between a
correct decoder and one that reads the digest's twenty-four characters back as a
boxed string, giving the whole saving away on every restore while every live
figure still looked right. Both halves are asserted against the BASELINE rather
than against each other, because two stores that had both rebuilt text would
agree with each other too.

THREE NEW GUARDS, FOR THE THINGS A BYTE FIGURE CANNOT SEE

`a_digest_equals_the_text_it_is_a_digest_of_and_refuses_a_corrupt_tag`: the
listing audit renders a projection from the live object and compares it to the
stored record, so a digest MUST equal the text it is a digest of. Going through
`as_text` instead would read a correctly written object as drifted -- that exact
bug happened once before, when this equality was derived instead of written. The
guard also asserts the shape survives the tag, not just the comparison, and that
five corrupt spellings are refused.

`two_records_spelling_the_same_thing_in_different_shapes_hash_differently`: a
digest and its own spelling held as text are EQUAL and hash DIFFERENTLY, because
the material states the shape first. Written down because the obvious tidy-up --
dropping the tag so equal values hash alike -- would make a corrupt snapshot
indistinguishable from a valid one.

AND A GUARD FROM ONE CHANGE AGO, POINTED AT WHAT NOW MATTERS

`a_projection_holds_nothing_for_an_object_under_no_hold` asserted that an object
under no restore, retention or legal hold owed no HEAP for them, because a stored
projection charged the audit for its box. Nothing stores a projection, so there is
no box, and `holds_heap_bytes` -- added one change ago for exactly that figure --
is dead code. The build under denied warnings is what said so.

What remains matters more than the figure did. The three holds are three of the
twelve rendered fields, so they have to reach the DIGEST: if `legal_hold()` ever
returned a constant, placing a legal hold would leave the record's value
unchanged, the object's own copy would still be right, every byte figure would
still be right, and the two services would silently disagree about an object
under a hold. The old guard could not have caught that.

So it is now `a_hold_reaches_the_digest_and_an_object_under_none_spells_zeros`,
and it flips ONE field on a clone of one object for each of the three holds --
exact isolation, because the rendered text carries no object name, so two metadata
values differing in a hold differ in nothing else. It also drives the write path:
placing a legal hold through the store must move the STORED record's value, not
just the renderer's output.

THREE MEASUREMENTS RETIRED, AND ONE THAT LOST A LEG

Retired, because their subjects are gone rather than because they broke: the one
pricing the projection's twelve fields, the one pricing how much of a record's
text was `name=value` scaffolding, and the one asking whether a snapshot's text
could be packed back into twelve values. All three are `#[ignore]`d, so nothing
runs them, and they would have sat printing zeros.

`measure_what_a_one_word_text_handle_would_save` counted a text handle inside the
stored projection. Nothing stores a projection, so that leg is not a saving
available to anyone: 64 inline became 56, and the published nets 48/32 became
40/24. A mirror of a type that stops being resident publishes a saving nobody can
collect, which is the third time a stale mirror has turned up in this file.

WHERE THE REMAINING FORTY BYTES ARE

  128  today
  120  the deleted flag into the version's top bit -- one bit in its own 8-byte word
  104  and the duplicate key handle dropped: the map and the record hold one
       allocation twice
   88  and the key's characters owned rather than shared, which is the one rung
       that carries a trade: it costs one copy per retained change, 40,960 bytes
       of fixed term, and pays above 2,560 objects

Below 88 is the object's own name and one handle to it. Eighty needs the records
map SPLIT BY KIND, so an object record's value slot can be eight bytes instead of
sixteen -- the slot is sixteen because a bucket record shares the map and its text
is the only copy there is.

ALSO, A MANGLED MESSAGE FROM THE CHANGE BEFORE THIS ONE

`a_storage_class_is_one_byte_and_its_wire_is_still_a_name` shipped with ten spaces
inside an assertion message. The hygiene check flagged it and I read past it.

THE FIGURES, EACH RE-MEASURED

  4 KiB row  3.11x -> 3.09x     1 KiB row  3.44x -> 3.38x
  striped    2.09x -> 2.03x     replicated 3.58x -> 3.51x
  metadata an object at 100 / 1,000 / 4,000 / 8,000   554 / 510 / 387 / 366
  delete trade   holding 1,799,649, emptied 2,268,662, 25.6% -> 26.0% larger
  the share table    objects 210 (60.7%), records 128 (37.0%), keys 8 (2.3%)

The feeds' share of metadata was published as two percentages of a total that
just moved; it is now the 163,866 bytes they hold at their bound, which is a
figure the line guard already pins.

MEASURED

Suite: 411 passing on the default feature set and 432 with every
feature, with the same 21 failures in both, name for name the baseline in
tools/known_failures.txt. Green on cargo build --all-targets for both feature sets with
warnings denied, and on cargo doc for both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@matrixarkai
matrixarkai merged commit 01ca46b into main Oct 24, 2026
2 of 3 checks passed
matrixarkai added a commit that referenced this pull request Oct 25, 2026
`.github/workflows/ci.yml` had two `run:` blocks whose line continuations were
written as the two characters `\n` instead of a backslash at end of line, so the
whole guard collapsed onto one line and `cargo test` was handed extra positional
arguments:

    cargo test --locked --lib n tests::filing_... -- --exact n | tee /tmp/guard.log

cargo refuses that before running anything:

    error: unexpected argument 'tests::filing_a_record_over_an_existing_key_replaces_it' found

So no test ran, `/tmp/guard.log` never got a `test result:` line, and the
`grep -q '^test result: ok. 1 passed'` that follows failed -- which is the step
failing for the right reason about the wrong thing. The guard was reporting
"did not run" and it was telling the truth.

Both affected guards are restored to the shape every other guard in the file
uses. Checked locally at this commit, each named test runs and passes on its own:

    running 1 test
    test tests::filing_a_record_over_an_existing_key_replaces_it ... ok
    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 459 filtered out

    running 1 test
    test tests::the_snapshot_decoder_accepts_the_version_before_this_one_and_refuses_the_one_after ... ok
    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 459 filtered out

The `format and rollback guards` job has failed on main since #219 for this and
nothing else; `build & docs` was green throughout. The two tests themselves were
never broken, which is why the failure survived four merges: the job was red, the
code was fine, and the step name named a test that was passing.

Co-authored-by: matrixarkai <mengmeng.xue@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants