blob storage for large atoms#985
Open
matthew-levan wants to merge 27 commits into
Open
Conversation
a bob atom is an indirect atom flagged with `u3a_blob_flag` in `len_w` whose `buf_w[0]` is the loom offset of its `u3a_blob` metadata. single-counter design: a blob file is deletable iff `use_w` == 0, where `use_w` is the sum of `eve_w` (event-log refs), `les_h` (king-held leases), and live bob-atom cardinality. the blob bank `blb_p` is a hamt in `u3v_home` keyed by `bid = (mug << 32) | seq`, marked during gc and rewritten during pack. `les_h` is transient ipc state and is zeroed at every boot.
content-addressed files at `.urb/bob/<mug>/<seq>` with bucket lockfiles and dedup on install; staging area `.urb/bob/stg/` for in-flight uploads, cleaned at boot. apis: save, load, `mmap`/unmap, wipe (removes empty buckets), walk (enumerates the store, skipping staging and lockfiles), and staging-file install. adds windows `mmap` support.
`ram` extends jam's bit-encoding with a 2-bit tag: normal atom, cell, backref, or blob ref (mat-encoded `mug` then `seq`) — bob atoms cross ipc as references, never as bytes. `tap` is the decoding side, materializing bob atoms via `u3i_blob`. wire format adds a 5-byte `"RAM\0"` + version header; `newt` carries the version byte.
atom jets that read raw bytes (`cut`, `met`, `can`, `cat`, `end`, `rap`, `rip`, `rsh`, `sew`, `swp`, hash and crypto jets, `bytestream`, `zlib`, ...) learn to source bytes from blob files instead of loom buffers. adds a `jam_shax` jet fusing jam and shax for blob hashing.
each committed event's blob ids are written to a `BLOBS` table keyed by event number. chop rebuilds `eve_w` for retained epochs (zero, scan, delete at `use_w` == 0) and reconciles `.urb/bob/` against the bank via `u3_blob_walk`, deleting orphaned files no accounting can reach.
leases are king-acquired, mars-issued: they keep a blob file alive only in the window before mars holds durable refs (event log / snapshot). the king stages a large file and sends a `%blob` writ; mars installs it and issues a lease (`les_h` += 1) with a 15-min ttl, tracked in a c-heap min-heap. the king renews via `%blas` every 5 min for every blob it still references, so a blob-bearing event pending in mars cannot outlive the ttl; renewal never resurrects a deleted blob's bank entry. the king releases via `%blrl` when its last reference dies. replay rebuilds `eve_w` per batch after the disk walk closes (a nested read txn on one thread fails with `MDB_BAD_RSLOT`), and commit warns loudly if an event references a blob missing from the bank.
http response bodies backed by bob atoms `mmap` the blob file and slice the mapping into the body chain, never materializing the bytes in the king's loom; inbound bodies over `U3_BLOB_THRESH` go straight to the blob store. unix syncs of large files stage to `.urb/bob/stg/` and install via the `%blob` writ, injecting the `%into` event from the install callback. mesa reads packet atoms through `u3r_view` (`mmap` when blob-backed) and stores large reassembled jumbo frames as blobs.
replaces the lazy zero-buffer init of `blb_p` (and the mid-v5 road additions `lop_p` and `cax.for_p`) with a versioned migration: v6 is an append-only change within the home page, so `_find_home` migrates a v5 snapshot in place and stamps `ver_d`. idempotent across a crash before the next checkpoint. bitness migrations are only defined for `U3V_VERLAT` snapshots — older snapshots must first be migrated by a same-bitness binary — keeping the migration paths linear instead of a version x bitness grid; both `disk.c` entry points reject older versions with an actionable error.
a blob file must survive until no event-log ref, lease, or loom atom references it. a fakezod lost a log-referenced blob after `|rm`, `|tomb`, `|meld` + ford-cache clear; this hardens every path involved: - `_mars_blob_del` refuses to delete while `eve_w` or `les_h` is nonzero even at `use_w` == 0 — that state is counter corruption, and we never destroy data on an accounting bug - new `u3a_blob_sane()` verifies `use_w` == `eve_w` + `les_h` + live bob-atom cardinality per bank entry: cheap tier (floor check) at `u3e_save` behind `u3o_check_corrupt`, deep tier (noun-graph walk with a c-heap verstable visited set, so the checker never allocates on the loom it checks) after every `|pack` and `|meld` - `_melt_cmp_atoms` compares bob atoms by `buf_w[0]` (same `u3a_blob` => same bid) instead of memcmp with raw `len_w`, which carried `u3a_blob_flag` and read ~2^31 words off the end - `u3u_cram` serializes through the blob-aware jam (`u3s_jam_xeno`): bob atoms are expanded into the rock as regular atoms, bytes mmap'd straight from the blob store; rocks stay portable and the loom — including `blb_p` — is untouched. formerly it routed through `_cu_realloc`, which repaved the home road (wiping the bank) and read bob atoms' `len_w` raw - deletes the deprecated `u3u_meld` and the now-unused `_cu_*` loom<->ur machinery - tests: `_test_sane` (checker catches both corruption directions), `_test_meld` (real `u3_meld_all` over duplicate bob atoms: unification, intact counts, valid pointer post-pack, and no deletion request while the log ref is held)
rock import previously materialized cram-expanded blobs as plain loom atoms (a 300mb blob needed 300mb of loom and bloated the snapshot), and `u3u_uncram`'s repave wiped the blob bank, losing `eve_w` for every log-referenced blob on the imported pier. - serial: adds `u3s_bsink` and `u3s_cue_xeno_blob()`: when a sink is installed on a cue handle, atoms over the threshold stream from the jam bitstream through the sink in 1mb byte-multiple chunks (`ur_bsr_bytes_any` chunks concatenate exactly; the final chunk carries the bit remainder) — the atom never touches the loom. the sink's `don_f` supplies the decoded atom, which enters the cue dictionary so backrefs share it. sink failure fails the cue: the bits are consumed, so there is no fallback - blob: adds `u3_blob_bsink`, the sink implementation: staging file under `.urb/bob/stg/`, then `u3_blob_move_stg` (mug, content dedup, rename) and `u3i_blob` — files surviving from before the import are re-linked rather than duplicated - disk: adds `u3_disk_blob_refs()`, rebuilding `eve_w` from the lmdb `BLOBS` table over the retained log; the scan callback now creates missing bank entries (queu's repave empties the bank) - queu: wires it together — sink + `U3_BLOB_THRESH` through `u3u_uncram`, blobified-count report, `eve_w` rebuild, and a deep `u3a_blob_sane()` check before the save. importing a rock with large atoms no longer needs `--loom` headroom - tests: `_test_cue_blob` covers streaming, backref sharing, byte round-trip, bank cardinality, and store dedup across re-cues rocks stay fully self-contained (cram still expands blob bytes into the jam) as a platform-independent way to move state; a ram-format rock with blob refs was considered and rejected for that reason.
a blob file missing from the store while the log still references it means the replayed range cannot reproduce its state. previously the `eve_w` rebuild silently recreated the bank entry and replay "succeeded", deferring the failure to whenever something first read the blob's bytes. `_mars_play_blobs` now checks `u3_blob_live` for every blob ref in the replayed range, reports each missing file with its event number (the scan continues, so all missing blobs are listed), and returns `c3n`; `_mars_play_batch` turns that into `_play_bad_e`, which exits nonzero. with the deletion guard, chop orphan sweep, and lease renewal in place, this state should only be reachable by external interference (manual deletion, partial backup restore) — exactly what a hard failure at boot is for.
`les_h` was the one leg of `use_w` with no durable backing: it lived
only in the loom snapshot and a C-heap PQ, and was zeroed on boot. A
mars crash in the `%blob` install -> commit window therefore lost the
king's lease, and boot could not safely reconcile orphans (a leaked file
and a still-leased file were indistinguishable), so reclamation was
deferred to `|chop`.
- lmdb: adds a `LEASES` table (`MDB_DUPSORT`, keyed by `bid`, value
`[exp, lea]`). duplicate keys let multiple live leases coexist on one
blob; `lea` is a unique id that disambiguates them so identical
expiries do not collapse and a specific row can be deleted. `maxdbs`
3 -> 4. apis: save, delete (exact row, idempotent), full-scan walk
- mars: `_mars_lease_take` commits the row before the install is acked,
rolling back the in-memory bump on failure; the sweeper and `%blrl`
drop the row when a lease expires or is released. `%blrl` now retires
exactly one live unit via `_mars_pq_kill_one`, fixing a latent
double-decrement (it never actually marked its PQ entry dead)
- mars: `_mars_play_leases` rebuilds `les_h` from the table at boot,
preserving each lease's remaining TTL and pruning expired/dead rows
(collect-then-delete to avoid `MDB_BAD_RSLOT`)
- disk: factors chop's reconciliation into `u3_disk_blob_gc` and runs it
at boot (after replay rebuilds `eve_w` and leases restore `les_h`), so
unreferenced blobs and orphan files are reclaimed promptly
- blob: fsyncs the bucket directory after the staging rename, so a
committed lease row can never reference a lost directory entry
- tests: `_test_lease{,_persist,_dups}` cover the round-trip, durability
across a close/reopen, `BLOBS` coexistence, and the multi-lease-per-bid
invariant
…cr_sing_atom` future blob work
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Blob Store for Large Atoms
Summary
Introduces a blob store that lets Urbit handle atoms larger than 32 MiB
without ever holding their bytes on the loom. Such atoms are stored as
content-addressed files under
$pier/.urb/bob/and referenced on the loom by abob atom — an indirect atom whose body points at small loom-resident
metadata instead of the bytes themselves. Serialization, the event log, IPC,
Clay sync, HTTP, and Ames all handle bob atoms transparently, materializing
bytes from disk (zero-copy via
mmap) only when a jet actually needs them.Motivation
The loom is a fixed-size memory-mapped region. Before this change, committing a
2 GiB file via
|commit %baserequired allocating a 2 GiB atom on the loom,jamming it for the event log, and shipping the jammed bytes over the king/serf
pipe — multiplying peak memory and making large-file support impractical.
Design
Bob atoms and the blob bank (
pkg/noun/allocate.h,imprison.c,retrieve.c)A bob atom is an indirect atom whose
len_wMSB is set (u3a_blob_flag); itsbuf_w[0]holds the loom offset of au3a_blobmetadata record rather thandata. The bank
blb_pis a HAMT inu3v_homekeyed bybid = (mug_h << 32) | seq_h, marked during GC and rewritten during pack.The design uses a single reference counter. A blob file is deletable iff
use_w == 0, whereuse_w = eve_w + les_h + live bob-atom cardinality:eve_w— event-log refcount (rebuilt from the LMDBBLOBStable onreplay/chop)
les_h— active king-held leases (durable in the LMDBLEASEStable; theloom copy is zeroed every boot and rebuilt from that table)
only on atom alloc/free
u3i_blob(mug, seq)allocates a bob atom. Retrieve functions (u3r_met,u3r_bytes,u3r_mug,u3r_sing, …) and the new zero-copyu3r_viewdetectbob atoms and source bytes from disk on demand.
_ca_take_atommaskslen_wwith
u3a_blob_maskso bob atoms copy correctly across road transitions.Blob store (
pkg/vere/blob.c,blob.h)Content-addressed files at
.urb/bob/<mug>/<seq>. Per-bucket lockfiles hold thenext sequence number under
fcntladvisory locking; install dedups byte-for-byteagainst existing files in the bucket. A staging area
.urb/bob/stg/holdsin-flight uploads (cleaned at boot). The API covers save, load,
mmap/unmap,wipe (removes empty buckets), walk (enumerates the store), and staging-file
install. Includes a Windows
mmapshim. Earth (king) is the sole writer;Mars (serf) is read-only.
Ram/tap serialization (
pkg/noun/serial.c,serial.h)ramextends jam's bit-encoding with a 2-bit tag — normal atom, cell, backref,or blob ref (mat-encoded
mugthenseq) — so bob atoms cross IPC and theevent log as compact references, never as bytes.
tapdecodes, rebuilding bobatoms via
u3i_blob. The wire format prepends a 5-byte"RAM\0"+ versionheader. Jam/cue are unchanged; old VER1/VER2 jam data is still read via fallback.
A streaming cue (
u3s_cue_xeno_blob+u3_blob_bsink) lets rock importstream over-threshold atoms straight from the jam bitstream into the blob store
in 1 MiB chunks — the atom never touches the loom — so
|cram/rock import oflarge state no longer needs
--loomheadroom.IPC and king leases (
pkg/vere/newt.c,lord.c,mars.c,king.c,vere.h)The newt wire protocol gains a version byte (
0x00jam,0x01ram); all sendsuse ram, all receives try tap then fall back to cue.
Leases are king-acquired, mars-issued, keeping a blob alive only in the
window between a
%blobinstall and the commit of the event that references it.The king stages a large file and sends a
%blobwrit; mars installs it andissues a lease (
les_h += 1) with a 15-minute TTL tracked in a c-heap min-heap.The king renews via
%blasevery 5 minutes for blobs it still references andreleases via
%blrlwhen its last reference dies. Renewal never resurrects adeleted blob's bank entry.
Leases are durable. Each is a row in an LMDB
LEASEStable (MDB_DUPSORT,keyed by blob id; the value pairs the expiry with a unique lease id, so multiple
live leases coexist on one blob and a specific row can be deleted). The row is
committed before mars acks the
%blobinstall, so a mars crash in theinstall→commit window cannot lose a lease the king believes it holds.
%blrland TTL expiry delete the corresponding row;
%blrlretires exactly one leaseunit. At boot,
_find_homezeroes the snapshot'sles_hand_mars_play_leasesrebuilds it from the table, preserving each lease's remaining TTL and pruning
expired or file-missing rows. To make the durable row trustworthy,
u3_blob_move_stgnow fsyncs the bucket directory after the staging rename.LMDB ref tracking, chop, and boot GC (
pkg/vere/disk.c,db/lmdb.c)Each committed event's blob ids are written to a
BLOBStable keyed by eventnumber. Replay and
|choprebuildeve_wfrom that table over the retainedlog.
u3_disk_blob_gcreconciles.urb/bob/against the bank — deleting blobsat
use_w == 0and on-disk files with no bank entry — and now runs both under|chopand at boot (after replay rebuildseve_wand_mars_play_leasesrestores
les_h). Durable leases are what make boot-time reclamation safe: agenuinely orphaned file is no longer indistinguishable from one a live lease
still protects.
Versions and migration (
pkg/noun/version.h,disk.c)U3V_VER6(home-road format): addsblb_p._find_homemigrates a v5snapshot in place and stamps
ver_d; idempotent across a crash before thenext checkpoint. Bitness migrations are only defined for
U3V_VERLAT, keepingpaths linear rather than a version×bitness grid; both disk entry points reject
older snapshots with an actionable error.
U3E_VER3(epoch format):image.bin+ ram-encoded event log. VER2→VER3rolls over to a new epoch.
Blob-aware jets (
pkg/noun/jets/...)Atom jets that read raw bytes —
cut,met,can,cat,end,rap,rip,rsh,sew,swp, the hash/crypto jets (shax, sha1, blake, keccak,ripe, muk, adler, crc, aes, chacha, ed25519),
bytestream,zlib— sourcebytes from blob files (via
u3r_viewmmap) instead of loom buffers. Adds ajam_shaxjet that fuses jam and shax for blob hashing.IO: HTTP, Unix, Mesa (
pkg/vere/io/{http,unix,mesa}.c)mmapthe blob file and slicethe mapping into the body chain — bytes never enter the king's loom; inbound
bodies over threshold go straight to the blob store.
.urb/bob/stg/and install viathe
%blobwrit, injecting%intofrom the install callback;%ergowrite-back streams from the store file in fixed chunks; the soft "has it
changed?" check compares mugs without reading bytes.
u3r_view, and stores reassembled jumboframes over threshold as blobs.
Meld, cram, and replay integrity
|meld/|pack:_melt_cmp_atomscompares bob atoms bybid(not amemcmpthat would read off the end of the stub); a deep
u3a_blob_sane()walk runsafter every pack/meld, with a cheap floor check at
u3e_save.|cram: serializes through the blob-aware jam, expanding bob bytes (mmap'dfrom the store) into the rock so rocks stay portable; the home road is
untouched. Deprecated
u3u_meldand the unused_cu_*machinery are removed.it now fails replay hard (
_mars_play_blobsreports every missing file withits event number and exits nonzero) instead of silently deferring the error.
What this does not change
optimization with no Hoon/Arvo changes.
the jam).
Status
Implementation is feature-complete. Test suites accompany the major
subsystems:
blob_tests.c(incl._test_sane/_test_meldinvariant checks and_test_lease*covering durable-lease round-trip, persistence across a restart,and the multi-lease-per-blob invariant),
serial_tests.c(incl. streaming-cuecoverage),
retrieve_tests.c, andnewt_tests.c. CI runs full replay offixture piers in both 32- and 64-bit builds.
Known limitations
u3i_bytestakes ac3_w(uint32_t) length, capping a single materializedloom atom at 4 GiB. Blobs larger than this live on disk but cannot be fully
realized on the loom; jets that stream via
u3r_vieware not subject to this.cache within a single Nock event.
u3r_view(zero-copymmap) keeps thischeap, but repeated access still re-maps.
Future work
This PR establishes the blob store but only populates it at I/O ingress. Several
follow-ups extend it into a general large-atom memory tier:
Big-atom arithmetic / computation. Operating on a blob atom currently
round-trips the whole thing through the loom:
u3i_vint,u3r_mp, and friendscall
u3r_blob_loadto materialize before computing. We want operators and jetsthat compute directly against the mmap'd blob via the existing
u3r_viewzero-copy path, and — where a result is itself large — write it straight back to
the store instead of materializing on the loom. The cheap single-pass cases come
first (increment via a one-pass carry over the mmap, comparison, slicing/cut,
which already read zero-copy), expanding toward the operations that big atoms
actually flow through.
A
%blobhint. A Nock 11 hint, emitted from Hoon with~>, that lets appdevelopers tell the runtime "atoms produced in this scope are expected to be
large." It plumbs through the existing dynamic-hint machinery — whitelisted in
_n_bint, handled in_n_hint_fore/_n_hint_hind(pkg/noun/nock.c) —alongside hints like
%boutand%hela. It is purely advisory (no semanticchange to the computation): the runtime uses it to bias allocation and
blobification decisions for results computed under the hint, e.g.
blobify-on-produce even below the size threshold, or to prime the automatic
detector below.
Automatic blob detection in the event loop. Nothing blobifies during normal
event processing today. We want a post-poke pass — around
_mars_sure_feck(
pkg/vere/mars.c), afteru3v_pokeruns the kernel and before effects routeto drivers — that walks the new state and effects and externalizes atoms over
the threshold straight to the store, so a computation that produces a large atom
doesn't bloat the loom. This reuses
u3_blob_save(pkg/vere/blob.c),including its byte-for-byte
_blob_dedup, so a recomputed identical atom mapsback onto the existing
(mug, seq)rather than writing a duplicate. The%blobhint feeds this pass as an explicit signal.
A blobify sweep in
|pack. Extend|pack(u3m_pack,pkg/noun/manage.c)— or a sibling command — with a pass that, during loom compaction, scans
reachable atoms and blobifies any exceeding
U3_BLOB_THRESH, reclaiming loomspace. It fits naturally between the existing seek (
u3a_pack_seek) and move(
u3a_pack_move) phases. Unlike the inline detector, this runs over everythingreachable rather than just freshly-produced atoms, so it catches large atoms that
accumulated before the detector existed or slipped below its per-event view.
Together these let large atoms originate, persist, and be computed on without
ever being forced to occupy the loom in full.